Merge remote-tracking branch 'upstream/master' into refactor-keys

# Conflicts:
#	context.go
This commit is contained in:
flc1125 2025-05-21 19:12:29 +08:00
commit ab148b997a
49 changed files with 2352 additions and 1029 deletions

View File

@ -26,16 +26,22 @@ jobs:
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.58.1
version: v1.61.0
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ["1.21", "1.22"]
go: ["1.23", "1.24"]
test-tags:
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
[
"",
"-tags nomsgpack",
'--ldflags="-checklinkname=0" -tags sonic',
"-tags go_json",
"-race",
]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
@ -75,7 +81,3 @@ jobs:
uses: codecov/codecov-action@v4
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
- name: Format
if: matrix.go-version == '1.22.x'
run: diff -u <(echo -n) <(gofmt -d .)

View File

@ -21,7 +21,7 @@ jobs:
with:
go-version: "^1"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser

View File

@ -7,7 +7,7 @@ linters:
- durationcheck
- errcheck
- errorlint
- exportloopref
- copyloopvar
- gci
- gofmt
- goimports
@ -16,7 +16,10 @@ linters:
- nakedret
- nilerr
- nolintlint
- perfsprint
- revive
- testifylint
- usestdlibvars
- wastedassign
linters-settings:
@ -33,6 +36,14 @@ linters-settings:
- G112
- G201
- G203
perfsprint:
err-error: true
errorf: true
int-conversion: true
sprintf1: true
strconcat: true
testifylint:
enable-all: true
issues:
exclude-rules:

View File

@ -488,7 +488,7 @@
- [FIX] Refactor render
- [FIX] Reworked tests
- [FIX] logger now supports cygwin
- [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] Use X-Forwarded-For before X-Real-IP
- [FIX] time.Time binding (#904)
## Gin 1.1.4

View File

@ -5,7 +5,7 @@
[![Build Status](https://github.com/gin-gonic/gin/workflows/Run%20Tests/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
@ -30,7 +30,7 @@ If you need performance and good productivity, you will love Gin.
### Prerequisites
Gin requires [Go](https://go.dev/) version [1.21](https://go.dev/doc/devel/release#go1.21.0) or above.
Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above.
### Getting Gin
@ -73,7 +73,7 @@ func main() {
To run the code, use the `go run` command, like:
```sh
$ go run example.go
go run example.go
```
Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response!
@ -90,11 +90,11 @@ A number of ready-to-run examples demonstrating various use cases of Gin are ava
## Documentation
See the [API documentation on godoc.org](https://godoc.org/github.com/gin-gonic/gin).
See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gin).
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
- [English](https://gin-gonic.com/docs/)
- [English](https://gin-gonic.com/en/docs/)
- [简体中文](https://gin-gonic.com/zh-cn/docs/)
- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
- [日本語](https://gin-gonic.com/ja/docs/)
@ -102,6 +102,8 @@ The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in
- [한국어](https://gin-gonic.com/ko-kr/docs/)
- [Turkish](https://gin-gonic.com/tr/docs/)
- [Persian](https://gin-gonic.com/fa/docs/)
- [Português](https://gin-gonic.com/pt/docs/)
- [Russian](https://gin-gonic.com/ru/docs/)
### Articles
@ -112,7 +114,7 @@ The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks](/BENCHMARKS.md).
| Benchmark name | (1) | (2) | (3) | (4) |
| ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
| ------------------------------ | --------: | --------------: | -----------: | --------------: |
| BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** |
| BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
@ -151,7 +153,7 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
## Middleware
You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib) and [gin-gonic/contrib](https://github.com/gin-gonic/contrib).
## Uses

View File

@ -90,7 +90,7 @@ func TestBasicAuthSucceed(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
@ -109,7 +109,7 @@ func TestBasicAuth401(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
@ -129,7 +129,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
@ -147,7 +147,7 @@ func TestBasicAuthForProxySucceed(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
@ -166,7 +166,7 @@ func TestBasicAuthForProxy407(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)

View File

@ -14,21 +14,21 @@ import (
func BenchmarkOneRoute(B *testing.B) {
router := New()
router.GET("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func BenchmarkRecoveryMiddleware(B *testing.B) {
router := New()
router.Use(Recovery())
router.GET("/", func(c *Context) {})
runRequest(B, router, "GET", "/")
runRequest(B, router, http.MethodGet, "/")
}
func BenchmarkLoggerMiddleware(B *testing.B) {
router := New()
router.Use(LoggerWithWriter(newMockWriter()))
router.GET("/", func(c *Context) {})
runRequest(B, router, "GET", "/")
runRequest(B, router, http.MethodGet, "/")
}
func BenchmarkManyHandlers(B *testing.B) {
@ -37,7 +37,7 @@ func BenchmarkManyHandlers(B *testing.B) {
router.Use(func(c *Context) {})
router.Use(func(c *Context) {})
router.GET("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func Benchmark5Params(B *testing.B) {
@ -45,7 +45,7 @@ func Benchmark5Params(B *testing.B) {
router := New()
router.Use(func(c *Context) {})
router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {})
runRequest(B, router, "GET", "/param/path/to/parameter/john/12345")
runRequest(B, router, http.MethodGet, "/param/path/to/parameter/john/12345")
}
func BenchmarkOneRouteJSON(B *testing.B) {
@ -56,7 +56,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
router.GET("/json", func(c *Context) {
c.JSON(http.StatusOK, data)
})
runRequest(B, router, "GET", "/json")
runRequest(B, router, http.MethodGet, "/json")
}
func BenchmarkOneRouteHTML(B *testing.B) {
@ -68,7 +68,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.GET("/html", func(c *Context) {
c.HTML(http.StatusOK, "index", "hola")
})
runRequest(B, router, "GET", "/html")
runRequest(B, router, http.MethodGet, "/html")
}
func BenchmarkOneRouteSet(B *testing.B) {
@ -76,7 +76,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
router.GET("/ping", func(c *Context) {
c.Set("key", "value")
})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func BenchmarkOneRouteString(B *testing.B) {
@ -84,13 +84,13 @@ func BenchmarkOneRouteString(B *testing.B) {
router.GET("/text", func(c *Context) {
c.String(http.StatusOK, "this is a plain text")
})
runRequest(B, router, "GET", "/text")
runRequest(B, router, http.MethodGet, "/text")
}
func BenchmarkManyRoutesFist(B *testing.B) {
router := New()
router.Any("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func BenchmarkManyRoutesLast(B *testing.B) {
@ -103,7 +103,7 @@ func Benchmark404(B *testing.B) {
router := New()
router.Any("/something", func(c *Context) {})
router.NoRoute(func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func Benchmark404Many(B *testing.B) {
@ -118,7 +118,7 @@ func Benchmark404Many(B *testing.B) {
router.GET("/user/:id/:mode", func(c *Context) {})
router.NoRoute(func(c *Context) {})
runRequest(B, router, "GET", "/viewfake")
runRequest(B, router, http.MethodGet, "/viewfake")
}
type mockWriter struct {

View File

@ -8,9 +8,11 @@ package binding
import (
"bytes"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
@ -24,7 +26,7 @@ func TestBindingMsgPack(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
require.NoError(t, err)
data := buf.Bytes()
@ -38,20 +40,20 @@ func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body,
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
require.Error(t, err)
}
func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK))
assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2))
}

File diff suppressed because it is too large Load Diff

View File

@ -159,6 +159,14 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
if k, v := head(opt, "="); k == "default" {
setOpt.isDefaultExists = true
setOpt.defaultValue = v
// convert semicolon-separated default values to csv-separated values for processing in setByForm
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
}
}
@ -182,6 +190,38 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
return false, nil
}
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
return vs, nil
}
var sep string
switch cfTag {
case "csv":
sep = ","
case "ssv":
sep = " "
case "tsv":
sep = "\t"
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
}
totalLength := 0
for _, v := range vs {
totalLength += strings.Count(v, sep) + 1
}
newVs = make([]string, 0, totalLength)
for _, v := range vs {
newVs = append(newVs, strings.Split(v, sep)...)
}
return newVs, nil
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
@ -192,15 +232,46 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
case reflect.Slice:
if !ok {
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
}
}
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
return true, setSlice(vs, value, field)
case reflect.Array:
if !ok {
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
}
}
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
return true, setArray(vs, value, field)
default:
var val string
@ -210,6 +281,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if len(vs) > 0 {
val = vs[0]
if val == "" {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
return ok, err
@ -324,18 +398,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
}
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
case "unix", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
var t time.Time
switch tf {
case "unix":
t = time.Unix(tv, 0)
case "unixmilli":
t = time.UnixMilli(tv)
case "unixmicro":
t = time.UnixMicro(tv)
default:
t = time.Unix(0, tv)
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}

View File

@ -5,7 +5,8 @@
package binding
import (
"fmt"
"encoding/hex"
"errors"
"mime/multipart"
"reflect"
"strconv"
@ -14,6 +15,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMappingBaseTypes(t *testing.T) {
@ -58,7 +60,7 @@ func TestMappingBaseTypes(t *testing.T) {
field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
assert.NoError(t, err, testName)
require.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName)
@ -67,13 +69,15 @@ func TestMappingBaseTypes(t *testing.T) {
func TestMappingDefault(t *testing.T) {
var s struct {
Str string `form:",default=defaultVal"`
Int int `form:",default=9"`
Slice []int `form:",default=9"`
Array [1]int `form:",default=9"`
}
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "defaultVal", s.Str)
assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array)
@ -84,7 +88,7 @@ func TestMappingSkipField(t *testing.T) {
A int
}
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 0, s.A)
}
@ -95,7 +99,7 @@ func TestMappingIgnoreField(t *testing.T) {
B int `form:"-"`
}
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B)
@ -107,7 +111,7 @@ func TestMappingUnexportedField(t *testing.T) {
b int `form:"b"`
}
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b)
@ -118,7 +122,7 @@ func TestMappingPrivateField(t *testing.T) {
f int `form:"field"`
}
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 0, s.f)
}
@ -128,7 +132,7 @@ func TestMappingUnknownFieldType(t *testing.T) {
}
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
assert.Error(t, err)
require.Error(t, err)
assert.Equal(t, errUnknownType, err)
}
@ -137,7 +141,7 @@ func TestMappingURI(t *testing.T) {
F int `uri:"field"`
}
err := mapURI(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 6, s.F)
}
@ -146,16 +150,34 @@ func TestMappingForm(t *testing.T) {
F int `form:"field"`
}
err := mapForm(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 6, s.F)
}
func TestMappingFormFieldNotSent(t *testing.T) {
var s struct {
F string `form:"field,default=defVal"`
}
err := mapForm(&s, map[string][]string{})
require.NoError(t, err)
assert.Equal(t, "defVal", s.F)
}
func TestMappingFormWithEmptyToDefault(t *testing.T) {
var s struct {
F string `form:"field,default=DefVal"`
}
err := mapForm(&s, map[string][]string{"field": {""}})
require.NoError(t, err)
assert.Equal(t, "DefVal", s.F)
}
func TestMapFormWithTag(t *testing.T) {
var s struct {
F int `externalTag:"field"`
}
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 6, s.F)
}
@ -170,7 +192,7 @@ func TestMappingTime(t *testing.T) {
var err error
time.Local, err = time.LoadLocation("Europe/Berlin")
assert.NoError(t, err)
require.NoError(t, err)
err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"},
@ -179,7 +201,7 @@ func TestMappingTime(t *testing.T) {
"CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"},
})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
@ -194,14 +216,14 @@ func TestMappingTime(t *testing.T) {
Time time.Time `time_location:"wrong"`
}
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
assert.Error(t, err)
require.Error(t, err)
// wrong time value
var wrongTime struct {
Time time.Time
}
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
assert.Error(t, err)
require.Error(t, err)
}
func TestMappingTimeDuration(t *testing.T) {
@ -211,12 +233,12 @@ func TestMappingTimeDuration(t *testing.T) {
// ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
assert.Error(t, err)
require.Error(t, err)
}
func TestMappingSlice(t *testing.T) {
@ -226,17 +248,17 @@ func TestMappingSlice(t *testing.T) {
// default value
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice)
// ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice)
// error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
assert.Error(t, err)
require.Error(t, err)
}
func TestMappingArray(t *testing.T) {
@ -246,20 +268,125 @@ func TestMappingArray(t *testing.T) {
// wrong default
err := mappingByPtr(&s, formSource{}, "form")
assert.Error(t, err)
require.Error(t, err)
// ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
assert.Error(t, err)
require.Error(t, err)
// error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
assert.Error(t, err)
require.Error(t, err)
}
func TestMappingCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
SliceTsv []int `form:"slice_tsv" collection_format:"tsv"`
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
ArrayTsv [2]int `form:"array_tsv" collection_format:"tsv"`
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{
"slice_multi": {"1", "2"},
"slice_csv": {"1,2"},
"slice_ssv": {"1 2"},
"slice_tsv": {"1 2"},
"slice_pipes": {"1|2"},
"array_multi": {"1", "2"},
"array_csv": {"1,2"},
"array_ssv": {"1 2"},
"array_tsv": {"1 2"},
"array_pipes": {"1|2"},
}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2}, s.SliceMulti)
assert.Equal(t, []int{1, 2}, s.SliceCsv)
assert.Equal(t, []int{1, 2}, s.SliceSsv)
assert.Equal(t, []int{1, 2}, s.SliceTsv)
assert.Equal(t, []int{1, 2}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
}
func TestMappingCollectionFormatInvalid(t *testing.T) {
var s struct {
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
}
err := mappingByPtr(&s, formSource{
"slice_csv": {"1,2"},
}, "form")
require.Error(t, err)
var s2 struct {
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
}
err = mappingByPtr(&s2, formSource{
"array_csv": {"1,2"},
}, "form")
require.Error(t, err)
}
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
assert.Equal(t, []int{1, 2, 3}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
}
func TestMappingStructField(t *testing.T) {
@ -270,7 +397,7 @@ func TestMappingStructField(t *testing.T) {
}
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 9, s.J.I)
}
@ -288,20 +415,20 @@ func TestMappingPtrField(t *testing.T) {
// With 0 items.
var req0 ptrRequest
err = mappingByPtr(&req0, formSource{}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Empty(t, req0.Items)
// With 1 item.
var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Len(t, req1.Items, 1)
assert.EqualValues(t, 1, req1.Items[0].Key)
// With 2 items.
var req2 ptrRequest
err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Len(t, req2.Items, 2)
assert.EqualValues(t, 1, req2.Items[0].Key)
assert.EqualValues(t, 2, req2.Items[1].Key)
@ -313,7 +440,7 @@ func TestMappingMapField(t *testing.T) {
}
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M)
}
@ -324,7 +451,7 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
var s S
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
require.NoError(t, err)
}
type customUnmarshalParamHex int
@ -343,7 +470,7 @@ func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
Foo customUnmarshalParamHex `form:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
@ -353,7 +480,7 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
Foo customUnmarshalParamHex `uri:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
assert.NoError(t, err)
require.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
@ -367,7 +494,7 @@ type customUnmarshalParamType struct {
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
parts := strings.Split(param, ":")
if len(parts) != 3 {
return fmt.Errorf("invalid format")
return errors.New("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
@ -380,7 +507,7 @@ func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
FileData customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
@ -392,7 +519,7 @@ func TestMappingCustomStructTypeWithURITag(t *testing.T) {
FileData customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err)
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
@ -404,7 +531,7 @@ func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
FileData *customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err)
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
@ -416,9 +543,95 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
FileData *customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err)
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
type customPath []string
func (p *customPath) UnmarshalParam(param string) error {
elems := strings.Split(param, "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
func TestMappingCustomSliceUri(t *testing.T) {
var s struct {
FileData customPath `uri:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceForm(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
}
type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error {
oid, err := convertTo(param)
if err != nil {
return err
}
*o = oid
return nil
}
func convertTo(s string) (objectID, error) {
var nilObjectID objectID
if len(s) != 24 {
return nilObjectID, errors.New("invalid format")
}
var oid [12]byte
_, err := hex.Decode(oid[:], []byte(s))
if err != nil {
return nilObjectID, err
}
return oid, nil
}
func TestMappingCustomArrayUri(t *testing.T) {
var s struct {
FileData objectID `uri:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
}
func TestMappingCustomArrayForm(t *testing.T) {
var s struct {
FileData objectID `form:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
}

View File

@ -12,6 +12,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFormMultipartBindingBindOneFile(t *testing.T) {
@ -27,7 +28,7 @@ func TestFormMultipartBindingBindOneFile(t *testing.T) {
req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
require.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file)
@ -53,7 +54,7 @@ func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
require.NoError(t, err)
assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files))
@ -90,7 +91,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
} {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err)
require.Error(t, err)
}
}
@ -106,17 +107,17 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
mw := multipart.NewWriter(&body)
for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err)
require.NoError(t, err)
n, err := fw.Write(file.Content)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, len(file.Content), n)
}
err := mw.Close()
assert.NoError(t, err)
require.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, "/", &body)
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req
@ -127,12 +128,12 @@ func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file test
assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open()
assert.NoError(t, err)
require.NoError(t, err)
body, err := io.ReadAll(fl)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
require.NoError(t, err)
}

View File

@ -34,7 +34,7 @@ func (protobufBinding) BindBody(body []byte, obj any) error {
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but util now we can't add
// Here it's same to return validate(obj), but until now we can't add
// `binding:""` to the struct which automatically generate by gen-proto
return nil
// return validate(obj)

View File

@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
if err := decoder.Decode(obj); err != nil {
return err
}
return decoder.Decode(obj)
return validate(obj)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testInterface interface {
@ -113,10 +114,10 @@ func TestValidateNoValidationValues(t *testing.T) {
test := createNoValidationValues()
empty := structNoValidationValues{}
assert.Nil(t, validate(test))
assert.Nil(t, validate(&test))
assert.Nil(t, validate(empty))
assert.Nil(t, validate(&empty))
require.NoError(t, validate(test))
require.NoError(t, validate(&test))
require.NoError(t, validate(empty))
require.NoError(t, validate(&empty))
assert.Equal(t, origin, test)
}
@ -163,8 +164,8 @@ func TestValidateNoValidationPointers(t *testing.T) {
//assert.Nil(t, validate(test))
//assert.Nil(t, validate(&test))
assert.Nil(t, validate(empty))
assert.Nil(t, validate(&empty))
require.NoError(t, validate(empty))
require.NoError(t, validate(&empty))
//assert.Equal(t, origin, test)
}
@ -173,22 +174,22 @@ type Object map[string]any
func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1}
assert.NoError(t, validate(obj))
assert.NoError(t, validate(&obj))
require.NoError(t, validate(obj))
require.NoError(t, validate(&obj))
assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
assert.NoError(t, validate(obj2))
assert.NoError(t, validate(&obj2))
require.NoError(t, validate(obj2))
require.NoError(t, validate(&obj2))
nu := 10
assert.NoError(t, validate(nu))
assert.NoError(t, validate(&nu))
require.NoError(t, validate(nu))
require.NoError(t, validate(&nu))
assert.Equal(t, 10, nu)
str := "value"
assert.NoError(t, validate(str))
assert.NoError(t, validate(&str))
require.NoError(t, validate(str))
require.NoError(t, validate(&str))
assert.Equal(t, "value", str)
}
@ -212,8 +213,8 @@ func TestValidateAndModifyStruct(t *testing.T) {
s := structModifyValidation{Integer: 1}
errs := validate(&s)
assert.Nil(t, errs)
assert.Equal(t, s, structModifyValidation{Integer: 0})
require.NoError(t, errs)
assert.Equal(t, structModifyValidation{Integer: 0}, s)
}
// structCustomValidation is a helper struct we use to check that
@ -239,14 +240,14 @@ func TestValidatorEngine(t *testing.T) {
err := engine.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error
assert.Nil(t, err)
require.NoError(t, err)
// Create an instance which will fail validation
withOne := structCustomValidation{Integer: 1}
errs := validate(withOne)
// Check that we got back non-nil errs
assert.NotNil(t, errs)
require.Error(t, errs)
// Check that the error matches expectation
assert.Error(t, errs, "", "", "notone")
require.Error(t, errs, "", "", "notone")
}

View File

@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"math"
"mime/multipart"
@ -187,10 +188,9 @@ func (c *Context) FullPath() string {
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] == nil {
continue
}
if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
c.index++
}
}
@ -292,76 +292,153 @@ func (c *Context) MustGet(key any) any {
panic(fmt.Errorf("key \"%v\" does not exist", key))
}
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key any) (s string) {
func getTyped[T any](c *Context, key any) (res T) {
if val, ok := c.Get(key); ok && val != nil {
s, _ = val.(string)
res, _ = val.(T)
}
return
}
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key any) (s string) {
return getTyped[string](c, key)
}
// GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key any) (b bool) {
if val, ok := c.Get(key); ok && val != nil {
b, _ = val.(bool)
}
return
return getTyped[bool](c, key)
}
// GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key any) (i int) {
if val, ok := c.Get(key); ok && val != nil {
i, _ = val.(int)
}
return
return getTyped[int](c, key)
}
// GetInt64 returns the value associated with the key as an integer.
// GetInt8 returns the value associated with the key as an integer 8.
func (c *Context) GetInt8(key any) (i8 int8) {
return getTyped[int8](c, key)
}
// GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key any) (i16 int16) {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key any) (i32 int32) {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key any) (i64 int64) {
if val, ok := c.Get(key); ok && val != nil {
i64, _ = val.(int64)
}
return
return getTyped[int64](c, key)
}
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key any) (ui uint) {
if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
return getTyped[uint](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer.
// GetUint8 returns the value associated with the key as an unsigned integer 8.
func (c *Context) GetUint8(key any) (ui8 uint8) {
return getTyped[uint8](c, key)
}
// GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key any) (ui16 uint16) {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key any) (ui32 uint32) {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key any) (ui64 uint64) {
if val, ok := c.Get(key); ok && val != nil {
ui64, _ = val.(uint64)
}
return
return getTyped[uint64](c, key)
}
// GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key any) (f32 float32) {
return getTyped[float32](c, key)
}
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key any) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
f64, _ = val.(float64)
}
return
return getTyped[float64](c, key)
}
// GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key any) (t time.Time) {
if val, ok := c.Get(key); ok && val != nil {
t, _ = val.(time.Time)
}
return
return getTyped[time.Time](c, key)
}
// GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key any) (d time.Duration) {
if val, ok := c.Get(key); ok && val != nil {
d, _ = val.(time.Duration)
}
return
return getTyped[time.Duration](c, key)
}
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) (is []int) {
return getTyped[[]int](c, key)
}
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key any) (i8s []int8) {
return getTyped[[]int8](c, key)
}
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key any) (i16s []int16) {
return getTyped[[]int16](c, key)
}
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key any) (i32s []int32) {
return getTyped[[]int32](c, key)
}
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key any) (i64s []int64) {
return getTyped[[]int64](c, key)
}
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key any) (uis []uint) {
return getTyped[[]uint](c, key)
}
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key any) (ui8s []uint8) {
return getTyped[[]uint8](c, key)
}
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key any) (ui16s []uint16) {
return getTyped[[]uint16](c, key)
}
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key any) (ui32s []uint32) {
return getTyped[[]uint32](c, key)
}
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key any) (ui64s []uint64) {
return getTyped[[]uint64](c, key)
}
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key any) (f32s []float32) {
return getTyped[[]float32](c, key)
}
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key any) (f64s []float64) {
return getTyped[[]float64](c, key)
}
// GetStringSlice returns the value associated with the key as a slice of strings.
@ -374,26 +451,18 @@ func (c *Context) GetStringSlice(key any) (ss []string) {
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key any) (sm map[string]any) {
if val, ok := c.Get(key); ok && val != nil {
sm, _ = val.(map[string]any)
}
return
return getTyped[map[string]any](c, key)
}
// GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key any) (sms map[string]string) {
if val, ok := c.Get(key); ok && val != nil {
sms, _ = val.(map[string]string)
}
return
return getTyped[map[string]string](c, key)
}
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key any) (smss map[string][]string) {
if val, ok := c.Get(key); ok && val != nil {
smss, _ = val.(map[string][]string)
}
return
return getTyped[map[string][]string](c, key)
}
/************************************/
@ -615,14 +684,22 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
}
// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0o750); err != nil {
var mode os.FileMode = 0o750
if len(perm) > 0 {
mode = perm[0]
}
dir := filepath.Dir(dst)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if err = os.Chmod(dir, mode); err != nil {
return err
}
@ -812,14 +889,14 @@ func (c *Context) ShouldBindBodyWithTOML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.TOML)
}
// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain).
func (c *Context) ShouldBindBodyWithPlain(obj any) error {
return c.ShouldBindBodyWith(obj, binding.Plain)
}
// ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string {
@ -957,6 +1034,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
})
}
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
func (c *Context) SetCookieData(cookie *http.Cookie) {
if cookie.Path == "" {
cookie.Path = "/"
}
if cookie.SameSite == http.SameSiteDefaultMode {
cookie.SameSite = c.sameSite
}
http.SetCookie(c.Writer, cookie)
}
// Cookie returns the named cookie provided in the request or
// ErrNoCookie if not found. And return the named cookie is unescaped.
// If multiple cookies match the given name, only one cookie will

File diff suppressed because it is too large Load Diff

View File

@ -78,7 +78,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.21+.
debugPrint(`[WARNING] Now Gin requires Go 1.23+.
`)
}

View File

@ -10,6 +10,7 @@ import (
"html/template"
"io"
"log"
"net/http"
"os"
"runtime"
"strings"
@ -17,6 +18,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO
@ -59,7 +61,7 @@ func TestDebugPrintError(t *testing.T) {
func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
@ -71,7 +73,7 @@ func TestDebugPrintRouteFunc(t *testing.T) {
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
@ -104,7 +106,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
if e == nil && m < ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.21+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.23+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
@ -154,13 +156,13 @@ func TestGetMinVer(t *testing.T) {
var m uint64
var e error
_, e = getMinVer("go1")
assert.NotNil(t, e)
require.Error(t, e)
m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m)
assert.Nil(t, e)
require.NoError(t, e)
m, e = getMinVer("go1.1.1")
assert.Nil(t, e)
require.NoError(t, e)
assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1")
assert.NotNil(t, e)
require.Error(t, e)
}

View File

@ -18,7 +18,7 @@ func TestBindWith(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct {
Foo string `form:"foo"`

View File

@ -26,6 +26,8 @@
- [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind default value if none provided](#bind-default-value-if-none-provided)
- [Collection format for arrays](#collection-format-for-arrays)
- [Bind Uri](#bind-uri)
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
- [Bind Header](#bind-header)
@ -68,7 +70,7 @@
### Build with json replacement
Gin uses `encoding/json` as default json package but you can change it by build from other tags.
Gin uses `encoding/json` as the default JSON package but you can change it by building from other tags.
[jsoniter](https://github.com/json-iterator/go)
@ -82,10 +84,10 @@ go build -tags=jsoniter .
go build -tags=go_json .
```
[sonic](https://github.com/bytedance/sonic) (you have to ensure that your cpu support avx instruction.)
[sonic](https://github.com/bytedance/sonic)
```sh
$ go build -tags="sonic avx" .
$ go build -tags=sonic .
```
### Build without `MsgPack` rendering feature
@ -118,7 +120,7 @@ func main() {
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
// By default it serves on :8080 unless a
// By default, it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
@ -170,7 +172,7 @@ func main() {
router := gin.Default()
// Query string parameters are parsed using the existing underlying request object.
// The request responds to an url matching: /welcome?firstname=Jane&lastname=Doe
// The request responds to a URL matching: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
@ -298,7 +300,7 @@ curl -X POST http://localhost:8080/upload \
#### Multiple files
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
```go
func main() {
@ -338,16 +340,16 @@ func main() {
router := gin.Default()
// Simple group: v1
v1 := router.Group("/v1")
{
v1 := router.Group("/v1")
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// Simple group: v2
v2 := router.Group("/v2")
{
v2 := router.Group("/v2")
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
@ -524,7 +526,7 @@ func main() {
return c.Writer.Status() < http.StatusInternalServerError
}
engine.Use(gin.LoggerWithConfig(loggerConfig))
router.Use(gin.LoggerWithConfig(loggerConfig))
router.Use(gin.Recovery())
// skipped
@ -702,7 +704,7 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
```
Skip validate: when running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
Skip-validation: Running the example above using the `curl` command returns an error. This is because the example uses `binding:"required"` for `Password`. If instead, you use `binding:"-"` for `Password`, then it will not return an error when you run the example again.
### Custom Validators
@ -830,6 +832,8 @@ type Person struct {
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmIcRo"` // case does not matter for "unix*" time formats
}
func main() {
@ -849,6 +853,8 @@ func startPage(c *gin.Context) {
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
log.Println(person.UnixMilliTime)
log.Println(person.UnixMicroTime)
}
c.String(http.StatusOK, "Success")
@ -858,7 +864,107 @@ func startPage(c *gin.Context) {
Test it with:
```sh
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012"
```
### Bind default value if none provided
If the server should bind a default value to a field when the client does not provide one, specify the default value using the `default` key within the `form` tag:
```
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name,default=William"`
Age int `form:"age,default=10"`
Friends []string `form:"friends,default=Will;Bill"`
Addresses [2]string `form:"addresses,default=foo bar" collection_format:"ssv"`
LapTimes []int `form:"lap_times,default=1;2;3" collection_format:"csv"`
}
func main() {
g := gin.Default()
g.POST("/person", func(c *gin.Context) {
var req Person
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusOK, req)
})
_ = g.Run("localhost:8080")
}
```
```
curl -X POST http://localhost:8080/person
{"Name":"William","Age":10,"Friends":["Will","Bill"],"Colors":["red","blue"],"LapTimes":[1,2,3]}
```
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
#### Collection format for arrays
| Format | Description | Example |
| --------------- | --------------------------------------------------------- | ----------------------- |
| multi (default) | Multiple parameter instances rather than multiple values. | key=foo&key=bar&key=baz |
| csv | Comma-separated values. | foo,bar,baz |
| ssv | Space-separated values. | foo bar baz |
| tsv | Tab-separated values. | "foo\tbar\tbaz" |
| pipes | Pipe-separated values. | foo\|bar\|baz |
```go
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Addresses []string `form:"addresses" collection_format:"csv"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Addresses)
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
}
c.String(200, "Success")
}
```
Test it with:
```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&addresses=foo,bar&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
```
### Bind Uri
@ -1081,7 +1187,7 @@ func main() {
})
r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
// You can also use a struct
var msg struct {
Name string `json:"user"`
Message string
@ -1287,13 +1393,19 @@ func main() {
### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles()
Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
```go
//go:embed templates/*
var templates embed.FS
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
//or
//router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
@ -1384,7 +1496,7 @@ You may use custom delims
#### Custom Template Funcs
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template).
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/template).
main.go
@ -1436,7 +1548,7 @@ Date: 2017/07/01
### Multitemplate
Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
Gin allows only one html.Template by default. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects
@ -1985,7 +2097,7 @@ type formB struct {
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
// Calling c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now.
@ -2192,12 +2304,64 @@ func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
// Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
```
You can also use the `SetCookieData` method, which accepts a `*http.Cookie` directly for more flexibility:
```go
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
}
fmt.Printf("Cookie value: %s \n", cookie)
@ -2218,7 +2382,7 @@ or network CIDRs from where clients which their request headers related to clien
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
IPv6 CIDRs.
**Attention:** Gin trust all proxies by default if you don't specify a trusted
**Attention:** Gin trusts all proxies by default if you don't specify a trusted
proxy using the function above, **this is NOT safe**. At the same time, if you don't
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
then `Context.ClientIP()` will return the remote address directly to avoid some

View File

@ -91,7 +91,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
}
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg *Error) Unwrap() error {
func (msg Error) Unwrap() error {
return msg.Err
}

View File

@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestError(t *testing.T) {
@ -122,7 +123,18 @@ func TestErrorUnwrap(t *testing.T) {
})
// check that 'errors.Is()' and 'errors.As()' behave as expected :
assert.True(t, errors.Is(err, innerErr))
require.ErrorIs(t, err, innerErr)
var testErr TestErr
assert.True(t, errors.As(err, &testErr))
require.ErrorAs(t, err, &testErr)
// Test non-pointer usage of gin.Error
errNonPointer := Error{
Err: innerErr,
Type: ErrorTypeAny,
}
wrappedErr := fmt.Errorf("wrapped: %w", errNonPointer)
// Check that 'errors.Is()' and 'errors.As()' behave as expected for non-pointer usage
require.ErrorIs(t, wrappedErr, innerErr)
var testErrNonPointer TestErr
require.ErrorAs(t, wrappedErr, &testErrNonPointer)
}

View File

@ -7,6 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
@ -28,7 +29,7 @@ func TestOnlyFilesFS_Open(t *testing.T) {
file, err := fs.Open("foo")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, testFile, file.(neutralizedReaddirFile).File)
}
@ -43,7 +44,7 @@ func TestOnlyFilesFS_Open_err(t *testing.T) {
file, err := fs.Open("foo")
assert.ErrorIs(t, err, testError)
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}
@ -52,7 +53,7 @@ func Test_neuteredReaddirFile_Readdir(t *testing.T) {
res, err := n.Readdir(0)
assert.NoError(t, err)
require.NoError(t, err)
assert.Nil(t, res)
}

45
gin.go
View File

@ -16,6 +16,7 @@ import (
"sync"
"github.com/gin-gonic/gin/internal/bytesconv"
filesystem "github.com/gin-gonic/gin/internal/fs"
"github.com/gin-gonic/gin/render"
"github.com/quic-go/quic-go/http3"
@ -24,6 +25,9 @@ import (
)
const defaultMultipartMemory = 32 << 20 // 32 MB
const escapedColon = "\\:"
const colon = ":"
const backslash = "\\"
var (
default404Body = []byte("404 page not found")
@ -282,6 +286,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ)
}
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
filesystem.FileSystem{FileSystem: fs}, patterns...))
engine.SetHTMLTemplate(templ)
}
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
@ -474,6 +491,26 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false
}
// updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
n.fullPath = strings.ReplaceAll(n.fullPath, escapedColon, colon)
n.indices = strings.ReplaceAll(n.indices, backslash, colon)
if n.children == nil {
return
}
for _, child := range n.children {
updateRouteTree(child)
}
}
// updateRouteTrees do update to the route trees
func (engine *Engine) updateRouteTrees() {
for _, tree := range engine.trees {
updateRouteTree(tree.root)
}
}
// parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP {
@ -498,7 +535,7 @@ func (engine *Engine) Run(addr ...string) (err error) {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
@ -575,7 +612,7 @@ func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
@ -614,10 +651,12 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Disclaimer: You can loop yourself to deal with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
oldHandlers := c.handlers
c.reset()
engine.handleHTTPRequest(c)
c.index = oldIndexValue
c.handlers = oldHandlers
}
func (engine *Engine) handleHTTPRequest(c *Context) {
@ -664,7 +703,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
break
}
if engine.HandleMethodNotAllowed {
if engine.HandleMethodNotAllowed && len(t) > 0 {
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
// containing a list of the target resource's currently supported methods.
allowed := make([]string, 0, len(t)-1)

View File

@ -32,6 +32,11 @@ func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...)
}
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
engine().LoadHTMLFS(fs, patterns...)
}
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ)
@ -154,7 +159,7 @@ func RunUnix(file string) (err error) {
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless on error happens.
// Note: the method will block the calling goroutine indefinitely unless an error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}

View File

@ -21,6 +21,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
@ -40,11 +41,11 @@ func testRequest(t *testing.T, params ...string) {
client := &http.Client{Transport: tr}
resp, err := client.Get(params[0])
assert.NoError(t, err)
require.NoError(t, err)
defer resp.Body.Close()
body, ioerr := io.ReadAll(resp.Body)
assert.NoError(t, ioerr)
require.NoError(t, ioerr)
var responseStatus = "200 OK"
if len(params) > 1 && params[1] != "" {
@ -73,13 +74,13 @@ func TestRunEmpty(t *testing.T) {
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":8080"))
require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example")
}
func TestBadTrustedCIDRs(t *testing.T) {
router := New()
assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
require.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
}
/* legacy tests
@ -87,7 +88,7 @@ func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080"))
require.Error(t, router.Run(":8080"))
}
func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
@ -100,7 +101,7 @@ func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunUnix(unixTestSocket))
require.Error(t, router.RunUnix(unixTestSocket))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
@ -112,15 +113,15 @@ func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
require.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
require.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunFd(int(socketFile.Fd())))
require.Error(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
@ -132,12 +133,12 @@ func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
require.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunListener(listener))
require.Error(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
@ -148,7 +149,7 @@ func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
require.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}
*/
@ -164,7 +165,7 @@ func TestRunTLS(t *testing.T) {
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
require.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
}
@ -201,7 +202,7 @@ func TestPusher(t *testing.T) {
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
require.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher")
}
@ -216,14 +217,14 @@ func TestRunEmptyWithEnv(t *testing.T) {
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":3123"))
require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example")
}
func TestRunTooMuchParams(t *testing.T) {
router := New()
assert.Panics(t, func() {
assert.NoError(t, router.Run("2", "2"))
require.NoError(t, router.Run("2", "2"))
})
}
@ -237,7 +238,7 @@ func TestRunWithPort(t *testing.T) {
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":5150"))
require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example")
}
@ -257,7 +258,7 @@ func TestUnixSocket(t *testing.T) {
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err)
require.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
@ -271,7 +272,7 @@ func TestUnixSocket(t *testing.T) {
func TestBadUnixSocket(t *testing.T) {
router := New()
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
require.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
}
func TestRunQUIC(t *testing.T) {
@ -286,7 +287,7 @@ func TestRunQUIC(t *testing.T) {
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
require.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
}
@ -294,15 +295,15 @@ func TestFileDescriptor(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
require.NoError(t, err)
socketFile, err := listener.File()
if isWindows() {
// not supported by windows, it is unimplemented now
assert.Error(t, err)
require.Error(t, err)
} else {
assert.NoError(t, err)
require.NoError(t, err)
}
if socketFile == nil {
@ -318,7 +319,7 @@ func TestFileDescriptor(t *testing.T) {
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
require.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
@ -332,15 +333,15 @@ func TestFileDescriptor(t *testing.T) {
func TestBadFileDescriptor(t *testing.T) {
router := New()
assert.Error(t, router.RunFd(0))
require.Error(t, router.RunFd(0))
}
func TestListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
require.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunListener(listener))
@ -350,7 +351,7 @@ func TestListener(t *testing.T) {
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
require.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
@ -365,11 +366,11 @@ func TestListener(t *testing.T) {
func TestBadListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err)
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
require.NoError(t, err)
listener.Close()
assert.Error(t, router.RunListener(listener))
require.Error(t, router.RunListener(listener))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
@ -395,7 +396,14 @@ func TestConcurrentHandleContext(t *testing.T) {
wg.Add(iterations)
for i := 0; i < iterations; i++ {
go func() {
testGetRequestHandler(t, router, "/")
req, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
wg.Done()
}()
}
@ -417,17 +425,6 @@ func TestConcurrentHandleContext(t *testing.T) {
// testRequest(t, "http://localhost:8033/example")
// }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}
func TestTreeRunDynamicRouting(t *testing.T) {
router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
@ -577,3 +574,28 @@ func TestTreeRunDynamicRouting(t *testing.T) {
func isWindows() bool {
return runtime.GOOS == "windows"
}
func TestEscapedColon(t *testing.T) {
router := New()
f := func(u string) {
router.GET(u, func(c *Context) { c.String(http.StatusOK, u) })
}
f("/r/r\\:r")
f("/r/r:r")
f("/r/r/:r")
f("/r/r/\\:r")
f("/r/r/r\\:r")
assert.Panics(t, func() {
f("\\foo:")
})
router.updateRouteTrees()
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/r/r123", "", "/r/r:r")
testRequest(t, ts.URL+"/r/r:r", "", "/r/r\\:r")
testRequest(t, ts.URL+"/r/r/r123", "", "/r/r/:r")
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
}

View File

@ -20,6 +20,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
)
@ -72,7 +73,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -130,7 +131,7 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -150,7 +151,7 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -177,7 +178,7 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -197,7 +198,7 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
@ -228,7 +229,7 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -248,7 +249,7 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -268,7 +269,7 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -295,7 +296,7 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@ -315,7 +316,116 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
var tmplFS = http.Dir("testdata/template")
func TestLoadHTMLFSTestMode(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSReleaseMode(t *testing.T) {
ts := setupHTMLFiles(
t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSUsingTLS(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSFuncMap(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
@ -326,31 +436,31 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
func TestAddRoute(t *testing.T) {
router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1)
assert.NotNil(t, router.trees.get("GET"))
assert.Nil(t, router.trees.get("POST"))
assert.NotNil(t, router.trees.get(http.MethodGet))
assert.Nil(t, router.trees.get(http.MethodPost))
router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2)
assert.NotNil(t, router.trees.get("GET"))
assert.NotNil(t, router.trees.get("POST"))
assert.NotNil(t, router.trees.get(http.MethodGet))
assert.NotNil(t, router.trees.get(http.MethodPost))
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2)
}
func TestAddRouteFails(t *testing.T) {
router := New()
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "/", HandlersChain{}) })
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
assert.Panics(t, func() {
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
})
}
@ -492,27 +602,27 @@ func TestListOfRoutes(t *testing.T) {
assert.Len(t, list, 7)
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/favicon.ico",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/users/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "POST",
Method: http.MethodPost,
Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
})
@ -530,7 +640,7 @@ func TestEngineHandleContext(t *testing.T) {
}
assert.NotPanics(t, func() {
w := PerformRequest(r, "GET", "/")
w := PerformRequest(r, http.MethodGet, "/")
assert.Equal(t, 301, w.Code)
})
}
@ -547,10 +657,10 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
r.GET("/:count", func(c *Context) {
countStr := c.Param("count")
count, err := strconv.Atoi(countStr)
assert.NoError(t, err)
require.NoError(t, err)
n, err := c.Writer.Write([]byte("."))
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, 1, n)
switch {
@ -563,7 +673,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
})
assert.NotPanics(t, func() {
w := PerformRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
w := PerformRequest(r, http.MethodGet, "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len())
})
@ -572,6 +682,44 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
// given
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
r := New()
v1 := r.Group("/v1")
{
v1.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounterV1, 1)
})
v1.GET("/test", func(c *Context) {
atomic.AddInt64(&handlerCounterV1, 1)
c.Status(http.StatusOK)
})
}
v2 := r.Group("/v2")
{
v2.GET("/test", func(c *Context) {
c.Request.URL.Path = "/v1/test"
r.HandleContext(c)
}, func(c *Context) {
atomic.AddInt64(&handlerCounterV2, 1)
})
}
// when
responseV1 := PerformRequest(r, "GET", "/v1/test")
responseV2 := PerformRequest(r, "GET", "/v2/test")
// then
assert.Equal(t, 200, responseV1.Code)
assert.Equal(t, 200, responseV2.Code)
assert.Equal(t, int64(2), handlerCounterV1)
assert.Equal(t, int64(2), middlewareCounterV1)
assert.Equal(t, int64(1), handlerCounterV2)
}
func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New()
@ -580,7 +728,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
@ -588,7 +736,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{
err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
assert.Error(t, err)
require.Error(t, err)
}
// valid ipv4 address
@ -597,7 +745,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies([]string{"192.168.1.33"})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
@ -605,7 +753,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{
err := r.SetTrustedProxies([]string{"192.168.1.256"})
assert.Error(t, err)
require.Error(t, err)
}
// valid ipv6 address
@ -613,7 +761,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
@ -621,7 +769,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.Error(t, err)
require.Error(t, err)
}
// valid ipv6 cidr
@ -629,7 +777,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
@ -637,7 +785,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
assert.Error(t, err)
require.Error(t, err)
}
// valid combination
@ -653,7 +801,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.1",
})
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
@ -665,7 +813,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.256",
})
assert.Error(t, err)
require.Error(t, err)
}
// nil value
@ -673,7 +821,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies(nil)
assert.Nil(t, r.trustedCIDRs)
assert.Nil(t, err)
require.NoError(t, err)
}
}
@ -699,7 +847,7 @@ func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) {
fc := func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
@ -711,8 +859,8 @@ func TestNewOptionFunc(t *testing.T) {
r := New(fc)
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
}
func TestWithOptionFunc(t *testing.T) {
@ -728,8 +876,8 @@ func TestWithOptionFunc(t *testing.T) {
})
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
}
type Birthday string
@ -748,9 +896,20 @@ func TestCustomUnmarshalStruct(t *testing.T) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
req := httptest.NewRequest(http.MethodGet, "/test?birthday=2000-01-01", nil)
w := httptest.NewRecorder()
route.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, `"2000/01/01"`, w.Body.String())
}
// Test the fix for https://github.com/gin-gonic/gin/issues/4002
func TestMethodNotAllowedNoRoute(t *testing.T) {
g := New()
g.HandleMethodNotAllowed = true
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}

View File

@ -10,10 +10,12 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type route struct {
@ -295,9 +297,9 @@ func TestShouldBindUri(t *testing.T) {
}
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, person.Name != "")
assert.True(t, person.ID != "")
require.NoError(t, c.ShouldBindUri(&person))
assert.NotEqual(t, "", person.Name)
assert.NotEqual(t, "", person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK")
})
@ -317,9 +319,9 @@ func TestBindUri(t *testing.T) {
}
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.BindUri(&person))
assert.True(t, person.Name != "")
assert.True(t, person.ID != "")
require.NoError(t, c.BindUri(&person))
assert.NotEqual(t, "", person.Name)
assert.NotEqual(t, "", person.ID)
c.String(http.StatusOK, "BindUri test OK")
})
@ -338,7 +340,7 @@ func TestBindUriError(t *testing.T) {
}
router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member
assert.Error(t, c.BindUri(&m))
require.Error(t, c.BindUri(&m))
})
path1, _ := exampleFromPath("/new/rest/:num")
@ -410,7 +412,7 @@ func exampleFromPath(path string) (string, Params) {
}
if start >= 0 {
if c == '/' {
value := fmt.Sprint(rand.Intn(100000))
value := strconv.Itoa(rand.Intn(100000))
params = append(params, Param{
Key: path[start:i],
Value: value,
@ -424,7 +426,7 @@ func exampleFromPath(path string) (string, Params) {
}
}
if start >= 0 {
value := fmt.Sprint(rand.Intn(100000))
value := strconv.Itoa(rand.Intn(100000))
params = append(params, Param{
Key: path[start:],
Value: value,

42
go.mod
View File

@ -1,48 +1,46 @@
module github.com/gin-gonic/gin
go 1.21.0
go 1.23.0
require (
github.com/bytedance/sonic v1.11.6
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.20.0
github.com/bytedance/sonic v1.13.2
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.26.0
github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.2.2
github.com/quic-go/quic-go v0.43.1
github.com/stretchr/testify v1.9.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.51.0
github.com/stretchr/testify v1.10.0
github.com/ugorji/go/codec v1.2.12
golang.org/x/net v0.25.0
google.golang.org/protobuf v1.34.1
golang.org/x/net v0.40.0
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kr/pretty v0.3.1 // 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/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.9.1 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)

96
go.sum
View File

@ -1,22 +1,21 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
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=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -25,16 +24,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
@ -44,10 +43,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
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=
@ -60,63 +55,54 @@ github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
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.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/quic-go v0.43.1 h1:fLiMNfQVe9q2JvSsiXo4fXOEguXHGGl9+6gLp4RPeZQ=
github.com/quic-go/quic-go v0.43.1/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

22
internal/fs/fs.go Normal file
View File

@ -0,0 +1,22 @@
package fs
import (
"io/fs"
"net/http"
)
// FileSystem implements an [fs.FS].
type FileSystem struct {
http.FileSystem
}
// Open passes `Open` to the upstream implementation and return an [fs.File].
func (o FileSystem) Open(name string) (fs.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
return fs.File(f), nil
}

49
internal/fs/fs_test.go Normal file
View File

@ -0,0 +1,49 @@
package fs
import (
"errors"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
open func(name string) (http.File, error)
}
func (m *mockFileSystem) Open(name string) (http.File, error) {
return m.open(name)
}
func TestFileSystem_Open(t *testing.T) {
var testFile *os.File
mockFS := &mockFileSystem{
open: func(name string) (http.File, error) {
return testFile, nil
},
}
fs := &FileSystem{mockFS}
file, err := fs.Open("foo")
require.NoError(t, err)
assert.Equal(t, testFile, file)
}
func TestFileSystem_Open_err(t *testing.T) {
testError := errors.New("mock")
mockFS := &mockFileSystem{
open: func(_ string) (http.File, error) {
return nil, testError
},
}
fs := &FileSystem{mockFS}
file, err := fs.Open("foo")
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
//go:build !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
package json

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build sonic && avx && (linux || windows || darwin) && amd64
//go:build sonic && (linux || windows || darwin)
package json

View File

@ -31,9 +31,9 @@ func TestLogger(t *testing.T) {
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
@ -41,21 +41,21 @@ func TestLogger(t *testing.T) {
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
PerformRequest(router, "POST", "/example")
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), http.MethodPost)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "PUT", "/example")
PerformRequest(router, http.MethodPut, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), http.MethodPut)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "DELETE", "/example")
PerformRequest(router, http.MethodDelete, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), http.MethodDelete)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
@ -77,9 +77,9 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "GET", "/notfound")
PerformRequest(router, http.MethodGet, "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/notfound")
}
@ -95,9 +95,9 @@ func TestLoggerWithConfig(t *testing.T) {
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
@ -105,21 +105,21 @@ func TestLoggerWithConfig(t *testing.T) {
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
PerformRequest(router, "POST", "/example")
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), http.MethodPost)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "PUT", "/example")
PerformRequest(router, http.MethodPut, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), http.MethodPut)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "DELETE", "/example")
PerformRequest(router, http.MethodDelete, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), http.MethodDelete)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
@ -141,9 +141,9 @@ func TestLoggerWithConfig(t *testing.T) {
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "GET", "/notfound")
PerformRequest(router, http.MethodGet, "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/notfound")
}
@ -169,12 +169,12 @@ func TestLoggerWithFormatter(t *testing.T) {
)
}))
router.GET("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
}
@ -210,12 +210,12 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
gotKeys = c.Keys
time.Sleep(time.Millisecond)
})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
@ -225,7 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, http.MethodGet, gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys)
@ -239,7 +239,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: false,
@ -250,7 +250,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: true,
@ -260,7 +260,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: true,
@ -271,7 +271,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: false,
@ -292,10 +292,10 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor()
}
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, blue, colorForMethod(http.MethodGet), "get should be blue")
assert.Equal(t, cyan, colorForMethod(http.MethodPost), "post should be cyan")
assert.Equal(t, yellow, colorForMethod(http.MethodPut), "put should be yellow")
assert.Equal(t, red, colorForMethod(http.MethodDelete), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
@ -329,13 +329,13 @@ func TestIsOutputColor(t *testing.T) {
}
consoleColorMode = autoColor
assert.Equal(t, true, p.IsOutputColor())
assert.True(t, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
assert.True(t, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
assert.False(t, p.IsOutputColor())
// test with isTerm flag false.
p = LogFormatterParams{
@ -343,13 +343,13 @@ func TestIsOutputColor(t *testing.T) {
}
consoleColorMode = autoColor
assert.Equal(t, false, p.IsOutputColor())
assert.False(t, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
assert.True(t, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
assert.False(t, p.IsOutputColor())
// reset console color mode.
consoleColorMode = autoColor
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
c.String(http.StatusInternalServerError, "hola!")
})
w := PerformRequest(router, "GET", "/error")
w := PerformRequest(router, http.MethodGet, "/error")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = PerformRequest(router, "GET", "/abort")
w = PerformRequest(router, http.MethodGet, "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = PerformRequest(router, "GET", "/print")
w = PerformRequest(router, http.MethodGet, "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
@ -389,11 +389,11 @@ func TestLoggerWithWriterSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
PerformRequest(router, "GET", "/logged")
PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "")
}
@ -407,11 +407,11 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
PerformRequest(router, "GET", "/logged")
PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "")
}
@ -427,11 +427,11 @@ func TestLoggerWithConfigSkipper(t *testing.T) {
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
PerformRequest(router, "GET", "/logged")
PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "")
}

View File

@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
signature += " XX "
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusOK, w.Code)
@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
signature += " X "
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
signature += " XX "
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -149,7 +149,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
@ -175,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusUnauthorized, w.Code)
@ -196,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
c.Next()
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusGone, w.Code)
@ -219,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C"
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
@ -246,7 +246,7 @@ func TestMiddlewareWrite(t *testing.T) {
})
})
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))

View File

@ -87,7 +87,7 @@ func TestPathCleanMallocs(t *testing.T) {
for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.EqualValues(t, allocs, 0)
assert.InDelta(t, 0, allocs, 0.01)
}
}

View File

@ -74,9 +74,9 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
key, _, _ := strings.Cut(header, ":")
if key == "Authorization" {
headers[idx] = key + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")

View File

@ -5,7 +5,6 @@
package gin
import (
"fmt"
"net"
"net/http"
"os"
@ -26,14 +25,14 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery",
w := PerformRequest(router, http.MethodGet, "/recovery",
header{
Key: "Host",
Value: "www.google.com",
},
header{
Key: "Authorization",
Value: fmt.Sprintf("Bearer %s", password),
Value: "Bearer " + password,
},
header{
Key: "Content-Type",
@ -56,7 +55,7 @@ func TestPanicInHandler(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@ -67,7 +66,7 @@ func TestPanicInHandler(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@ -84,7 +83,7 @@ func TestPanicWithAbort(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
}
@ -135,7 +134,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e)
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
@ -156,7 +155,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@ -167,7 +166,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@ -191,7 +190,7 @@ func TestCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@ -202,7 +201,7 @@ func TestCustomRecovery(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@ -226,7 +225,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@ -237,7 +236,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")

View File

@ -7,6 +7,8 @@ package render
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin/internal/fs"
)
// Delims represents a set of Left and Right delimiters for HTML template rendering.
@ -33,6 +35,8 @@ type HTMLProduction struct {
type HTMLDebug struct {
Files []string
Glob string
FileSystem http.FileSystem
Patterns []string
Delims Delims
FuncMap template.FuncMap
}
@ -73,7 +77,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
}
panic("the HTML debug render was created without files or glob pattern")
if r.FileSystem != nil && len(r.Patterns) > 0 {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
}
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
}
// Render (HTML) executes template and writes its result with custom ContentType for response.

View File

@ -9,6 +9,7 @@ import (
"fmt"
"html/template"
"net/http"
"unicode"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
@ -151,7 +152,7 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
}
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
func (r AsciiJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
if err != nil {
@ -159,12 +160,15 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
}
var buffer bytes.Buffer
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r)
if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r))
if r > unicode.MaxASCII {
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
buffer.Write(escapeBuf)
} else {
buffer.WriteByte(byte(r))
}
buffer.WriteString(cvt)
}
_, err = w.Write(buffer.Bytes())

View File

@ -12,6 +12,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
@ -29,7 +30,7 @@ func TestRenderMsgPack(t *testing.T) {
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
@ -37,7 +38,7 @@ func TestRenderMsgPack(t *testing.T) {
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -18,6 +18,7 @@ import (
"github.com/gin-gonic/gin/internal/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
@ -36,7 +37,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -46,7 +47,7 @@ func TestRenderJSONError(t *testing.T) {
data := make(chan int)
// json: unsupported type: chan int
assert.Error(t, (JSON{data}).Render(w))
require.Error(t, (JSON{data}).Render(w))
}
func TestRenderIndentedJSON(t *testing.T) {
@ -58,7 +59,7 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -69,7 +70,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
// json: unsupported type: chan int
err := (IndentedJSON{data}).Render(w)
assert.Error(t, err)
require.Error(t, err)
}
func TestRenderSecureJSON(t *testing.T) {
@ -83,7 +84,7 @@ func TestRenderSecureJSON(t *testing.T) {
err1 := (SecureJSON{"while(1);", data}).Render(w1)
assert.NoError(t, err1)
require.NoError(t, err1)
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
@ -95,7 +96,7 @@ func TestRenderSecureJSON(t *testing.T) {
}}
err2 := (SecureJSON{"while(1);", datas}).Render(w2)
assert.NoError(t, err2)
require.NoError(t, err2)
assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type"))
}
@ -106,7 +107,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
// json: unsupported type: chan int
err := (SecureJSON{"while(1);", data}).Render(w)
assert.Error(t, err)
require.Error(t, err)
}
func TestRenderJsonpJSON(t *testing.T) {
@ -120,7 +121,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1)
assert.NoError(t, err1)
require.NoError(t, err1)
assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
@ -132,7 +133,7 @@ func TestRenderJsonpJSON(t *testing.T) {
}}
err2 := (JsonpJSON{"x", datas}).Render(w2)
assert.NoError(t, err2)
require.NoError(t, err2)
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
}
@ -191,7 +192,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
e := (JsonpJSON{"", data}).Render(w)
assert.NoError(t, e)
require.NoError(t, e)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
@ -203,7 +204,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
// json: unsupported type: chan int
err := (JsonpJSON{"x", data}).Render(w)
assert.Error(t, err)
require.Error(t, err)
}
func TestRenderAsciiJSON(t *testing.T) {
@ -215,7 +216,7 @@ func TestRenderAsciiJSON(t *testing.T) {
err := (AsciiJSON{data1}).Render(w1)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
@ -223,7 +224,7 @@ func TestRenderAsciiJSON(t *testing.T) {
data2 := 3.1415926
err = (AsciiJSON{data2}).Render(w2)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "3.1415926", w2.Body.String())
}
@ -232,7 +233,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
data := make(chan int)
// json: unsupported type: chan int
assert.Error(t, (AsciiJSON{data}).Render(w))
require.Error(t, (AsciiJSON{data}).Render(w))
}
func TestRenderPureJSON(t *testing.T) {
@ -242,7 +243,7 @@ func TestRenderPureJSON(t *testing.T) {
"html": "<b>",
}
err := (PureJSON{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -283,7 +284,7 @@ b:
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -298,7 +299,7 @@ func (ft *fail) MarshalYAML() (any, error) {
func TestRenderYAMLFail(t *testing.T) {
w := httptest.NewRecorder()
err := (YAML{&fail{}}).Render(w)
assert.Error(t, err)
require.Error(t, err)
}
func TestRenderTOML(t *testing.T) {
@ -311,7 +312,7 @@ func TestRenderTOML(t *testing.T) {
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
err := (TOML{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -319,7 +320,7 @@ func TestRenderTOML(t *testing.T) {
func TestRenderTOMLFail(t *testing.T) {
w := httptest.NewRecorder()
err := (TOML{net.IPv4bcast}).Render(w)
assert.Error(t, err)
require.Error(t, err)
}
// test Protobuf rendering
@ -334,12 +335,12 @@ func TestRenderProtoBuf(t *testing.T) {
(ProtoBuf{data}).WriteContentType(w)
protoData, err := proto.Marshal(data)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
err = (ProtoBuf{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
@ -348,7 +349,7 @@ func TestRenderProtoBufFail(t *testing.T) {
w := httptest.NewRecorder()
data := &testdata.Test{}
err := (ProtoBuf{data}).Render(w)
assert.Error(t, err)
require.Error(t, err)
}
func TestRenderXML(t *testing.T) {
@ -362,14 +363,14 @@ func TestRenderXML(t *testing.T) {
err := (XML{data}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest("GET", "/test-redirect", nil)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
require.NoError(t, err)
data1 := Redirect{
Code: http.StatusMovedPermanently,
@ -379,7 +380,7 @@ func TestRenderRedirect(t *testing.T) {
w := httptest.NewRecorder()
err = data1.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
data2 := Redirect{
Code: http.StatusOK,
@ -390,7 +391,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder()
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
})
data3 := Redirect{
@ -401,7 +402,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder()
err = data3.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
// only improve coverage
data2.WriteContentType(w)
@ -416,7 +417,7 @@ func TestRenderData(t *testing.T) {
Data: data,
}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
}
@ -435,7 +436,7 @@ func TestRenderString(t *testing.T) {
Data: []any{"manu", 2},
}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "hola manu 2", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -448,7 +449,7 @@ func TestRenderStringLenZero(t *testing.T) {
Data: []any{},
}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "hola %s %d", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -464,7 +465,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
err := instance.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -480,7 +481,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
err := instance.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -490,6 +491,8 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
FileSystem: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
@ -499,7 +502,7 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
err := instance.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -509,6 +512,8 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
htmlRender := HTMLDebug{
Files: nil,
Glob: "../testdata/template/hello*",
FileSystem: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
@ -518,7 +523,28 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
err := instance.Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugFS(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: http.Dir("../testdata/template"),
Patterns: []string{"hello.tmpl"},
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
err := instance.Render(w)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -527,6 +553,8 @@ func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: nil,
Patterns: nil,
Delims: Delims{"{{", "}}"},
FuncMap: nil,
}
@ -548,7 +576,7 @@ func TestRenderReader(t *testing.T) {
Headers: headers,
}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
@ -571,7 +599,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
Headers: headers,
}).Render(w)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header())
@ -588,6 +616,6 @@ func TestRenderWriteError(t *testing.T) {
ResponseRecorder: httptest.NewRecorder(),
}
err := r.Render(ew)
assert.NotNil(t, err)
require.Error(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error())
}

View File

@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO
@ -95,13 +96,13 @@ func TestResponseWriterWrite(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWriter.Body.String())
assert.NoError(t, err)
require.NoError(t, err)
n, err = w.Write([]byte(" adios"))
assert.Equal(t, 6, n)
assert.Equal(t, 10, w.Size())
assert.Equal(t, "hola adios", testWriter.Body.String())
assert.NoError(t, err)
require.NoError(t, err)
}
func TestResponseWriterHijack(t *testing.T) {
@ -112,7 +113,7 @@ func TestResponseWriterHijack(t *testing.T) {
assert.Panics(t, func() {
_, _, err := w.Hijack()
assert.NoError(t, err)
require.NoError(t, err)
})
assert.True(t, w.Written())
@ -135,7 +136,7 @@ func TestResponseWriterFlush(t *testing.T) {
// should return 500
resp, err := http.Get(testServer.URL)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
}

View File

@ -13,6 +13,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type header struct {
@ -386,7 +387,7 @@ func TestRouteStaticFile(t *testing.T) {
}
defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err)
require.NoError(t, err)
f.Close()
dir, filename := filepath.Split(f.Name())
@ -421,7 +422,7 @@ func TestRouteStaticFileFS(t *testing.T) {
}
defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err)
require.NoError(t, err)
f.Close()
dir, filename := filepath.Split(f.Name())
@ -484,7 +485,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified"))
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
}
@ -522,8 +523,8 @@ func TestRouteNotAllowedEnabled3(t *testing.T) {
w := PerformRequest(router, http.MethodPut, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
allowed := w.Header().Get("Allow")
assert.Contains(t, allowed, "GET")
assert.Contains(t, allowed, "POST")
assert.Contains(t, allowed, http.MethodGet)
assert.Contains(t, allowed, http.MethodPost)
}
func TestRouteNotAllowedDisabled(t *testing.T) {
@ -556,10 +557,10 @@ func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := PerformRequest(router, "GET", tr.route)
w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
assert.Equal(t, tr.location, w.Header().Get("Location"))
}
}
}
@ -589,7 +590,7 @@ func TestRouterNotFound(t *testing.T) {
w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
assert.Equal(t, tr.location, w.Header().Get("Location"))
}
}
@ -785,6 +786,6 @@ func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) {
v1.GET("/orgs/:id", handlerTest1)
v1.DELETE("/orgs/:id", handlerTest1)
w := PerformRequest(r, "GET", "/base/v1/user/groups")
w := PerformRequest(r, http.MethodGet, "/base/v1/user/groups")
assert.Equal(t, http.StatusNotFound, w.Code)
}

33
tree.go
View File

@ -65,17 +65,10 @@ func (trees methodTrees) get(method string) *node {
return nil
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func longestCommonPrefix(a, b string) int {
i := 0
max := min(len(a), len(b))
for i < max && a[i] == b[i] {
max_ := min(len(a), len(b))
for i < max_ && a[i] == b[i] {
i++
}
return i
@ -205,7 +198,7 @@ walk:
}
// Check if a child with the next path byte exists
for i, max := 0, len(n.indices); i < max; i++ {
for i, max_ := 0, len(n.indices); i < max_; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
@ -241,7 +234,7 @@ walk:
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
pathSeg, _, _ = strings.Cut(pathSeg, "/")
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
@ -269,7 +262,19 @@ walk:
// Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start
escapeColon := false
for start, c := range []byte(path) {
if escapeColon {
escapeColon = false
if c == ':' {
continue
}
panic("invalid escape string in path '" + path + "'")
}
if c == '\\' {
escapeColon = true
continue
}
// A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' {
continue
@ -353,7 +358,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := ""
if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
}
panic("catch-all wildcard '" + path +
"' in new path '" + fullPath +
@ -364,7 +369,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
// currently fixed width 1 for '/'
i--
if path[i] != '/' {
if i < 0 || path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}
@ -770,7 +775,7 @@ walk: // Outer loop for walking the tree
// Runes are up to 4 byte long,
// -4 would definitely be another rune.
var off int
for max := min(npLen, 3); off < max; off++ {
for max_ := min(npLen, 3); off < max_; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:])

View File

@ -192,6 +192,7 @@ func TestTreeWildcard(t *testing.T) {
"/get/abc/123abg/:param",
"/get/abc/123abf/:param",
"/get/abc/123abfff/:param",
"/get/abc/escaped_colon/test\\:param",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
@ -315,6 +316,7 @@ func TestTreeWildcard(t *testing.T) {
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
})
checkPriorities(t, tree)
@ -419,6 +421,9 @@ func TestTreeWildcardConflict(t *testing.T) {
{"/id/:id", false},
{"/static/*file", false},
{"/static/", true},
{"/escape/test\\:d1", false},
{"/escape/test\\:d2", false},
{"/escape/test:param", false},
}
testRoutes(t, routes)
}
@ -971,3 +976,45 @@ func TestTreeWildcardConflictEx(t *testing.T) {
}
}
}
func TestTreeInvalidEscape(t *testing.T) {
routes := map[string]bool{
"/r1/r": true,
"/r2/:r": true,
"/r3/\\:r": true,
}
tree := &node{}
for route, valid := range routes {
recv := catchPanic(func() {
tree.addRoute(route, fakeHandler(route))
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
}
}
func TestWildcardInvalidSlash(t *testing.T) {
const panicMsgPrefix = "no / before catch-all in path"
routes := map[string]bool{
"/foo/bar": true,
"/foo/x*zy": false,
"/foo/b*r": false,
}
for route, valid := range routes {
tree := &node{}
recv := catchPanic(func() {
tree.addRoute(route, nil)
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
if rs, ok := recv.(string); recv != nil && (!ok || !strings.HasPrefix(rs, panicMsgPrefix)) {
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsgPrefix, route, recv)
}
}
}

View File

@ -29,7 +29,7 @@ type testStruct struct {
}
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
assert.Equal(t.T, "POST", req.Method)
assert.Equal(t.T, http.MethodPost, req.Method)
assert.Equal(t.T, "/path", req.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "hello")
@ -39,17 +39,17 @@ func TestWrap(t *testing.T) {
router := New()
router.POST("/path", WrapH(&testStruct{t}))
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "GET", req.Method)
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "/path2", req.URL.Path)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "hola!")
}))
w := PerformRequest(router, "POST", "/path")
w := PerformRequest(router, http.MethodPost, "/path")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hello", w.Body.String())
w = PerformRequest(router, "GET", "/path2")
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "hola!", w.Body.String())
}
@ -119,13 +119,13 @@ func TestBindMiddleware(t *testing.T) {
called = true
value = c.MustGet(BindKey).(*bindTestStruct)
})
PerformRequest(router, "GET", "/?foo=hola&bar=10")
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=10")
assert.True(t, called)
assert.Equal(t, "hola", value.Foo)
assert.Equal(t, 10, value.Bar)
called = false
PerformRequest(router, "GET", "/?foo=hola&bar=1")
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=1")
assert.False(t, called)
assert.Panics(t, func() {
@ -145,6 +145,6 @@ func TestMarshalXMLforH(t *testing.T) {
}
func TestIsASCII(t *testing.T) {
assert.Equal(t, isASCII("test"), true)
assert.Equal(t, isASCII("🧡💛💚💙💜"), false)
assert.True(t, isASCII("test"))
assert.False(t, isASCII("🧡💛💚💙💜"))
}