Merge branch 'master' into http3

This commit is contained in:
Bo-Yi Wu 2024-05-08 07:41:42 +08:00 committed by GitHub
commit 52092b2cca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1360 additions and 213 deletions

View File

@ -30,7 +30,7 @@ func main() {
<!-- Your expectation result of 'curl' command, like --> <!-- Your expectation result of 'curl' command, like -->
``` ```
$ curl http://localhost:8201/hello/world $ curl http://localhost:9000/hello/world
Hello world Hello world
``` ```
@ -38,7 +38,7 @@ Hello world
<!-- Actual result showing the problem --> <!-- Actual result showing the problem -->
``` ```
$ curl -i http://localhost:8201/hello/world $ curl -i http://localhost:9000/hello/world
<YOUR RESULT> <YOUR RESULT>
``` ```

View File

@ -7,12 +7,12 @@ name: "CodeQL"
on: on:
push: push:
branches: [ master ] branches: [master]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ master ] branches: [master]
schedule: schedule:
- cron: '0 17 * * 5' - cron: "0 17 * * 5"
jobs: jobs:
analyze: analyze:
@ -29,15 +29,15 @@ jobs:
# Override automatic language detection by changing the below list # Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
# TODO: Enable for javascript later # TODO: Enable for javascript later
language: [ 'go'] language: ["go"]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -46,4 +46,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@ -15,24 +15,28 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup go - name: Checkout
uses: actions/setup-go@v3 uses: actions/checkout@v4
with: with:
go-version: '^1.18' fetch-depth: 0
- name: Checkout repository - name: Set up Go
uses: actions/checkout@v3 uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
check-latest: true
- name: Setup golangci-lint - name: Setup golangci-lint
uses: golangci/golangci-lint-action@v3.4.0 uses: golangci/golangci-lint-action@v5
with: with:
version: v1.52.2 version: v1.56.2
args: --verbose args: --verbose
test: test:
needs: lint needs: lint
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
go: ['1.18', '1.19', '1.20'] go: ["1.18", "1.19", "1.20", "1.21", "1.22"]
test-tags: ['', '-tags nomsgpack', '-tags "sonic avx"', '-tags go_json'] test-tags:
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
go-build: ~/.cache/go-build go-build: ~/.cache/go-build
@ -46,16 +50,17 @@ jobs:
GOPROXY: https://proxy.golang.org GOPROXY: https://proxy.golang.org
steps: steps:
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
- uses: actions/cache@v3 - uses: actions/cache@v4
with: with:
path: | path: |
${{ matrix.go-build }} ${{ matrix.go-build }}
@ -68,10 +73,10 @@ jobs:
run: make test run: make test
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }} flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
- name: Format - name: Format
if: matrix.go-version == '1.20.x' if: matrix.go-version == '1.22.x'
run: diff -u <(echo -n) <(gofmt -d .) run: diff -u <(echo -n) <(gofmt -d .)

View File

@ -3,7 +3,7 @@ name: Goreleaser
on: on:
push: push:
tags: tags:
- '*' - "*"
permissions: permissions:
contents: write contents: write
@ -12,23 +12,20 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout uses: actions/checkout@v4
uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- - name: Set up Go
name: Set up Go uses: actions/setup-go@v5
uses: actions/setup-go@v3
with: with:
go-version: 1.20 go-version: "^1"
- - name: Run GoReleaser
name: Run GoReleaser uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v4
with: with:
# either 'goreleaser' (default) or 'goreleaser-pro' # either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser distribution: goreleaser
version: latest version: latest
args: release --rm-dist args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@ -5,3 +5,7 @@ count.out
test test
profile.out profile.out
tmp.out tmp.out
# Develop tools
.idea/
.vscode/

View File

@ -3,7 +3,6 @@ run:
linters: linters:
enable: enable:
- asciicheck - asciicheck
- depguard
- dogsled - dogsled
- durationcheck - durationcheck
- errcheck - errcheck

View File

@ -1,8 +1,7 @@
project_name: gin project_name: gin
builds: builds:
- - # If true, skip the build.
# If true, skip the build.
# Useful for library projects. # Useful for library projects.
# Default is false # Default is false
skip: true skip: true
@ -10,7 +9,7 @@ builds:
changelog: changelog:
# Set it to true if you wish to skip the changelog generation. # Set it to true if you wish to skip the changelog generation.
# This may result in an empty release notes on GitHub/GitLab/Gitea. # This may result in an empty release notes on GitHub/GitLab/Gitea.
skip: false disable: false
# Changelog generation implementation to use. # Changelog generation implementation to use.
# #
@ -21,7 +20,7 @@ changelog:
# - `github-native`: uses the GitHub release notes generation API, disables the groups feature. # - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
# #
# Defaults to `git`. # Defaults to `git`.
use: git use: github
# Sorts the changelog by the commit's messages. # Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty # Could either be asc, desc or empty
@ -38,20 +37,20 @@ changelog:
- title: Features - title: Features
regexp: "^.*feat[(\\w)]*:+.*$" regexp: "^.*feat[(\\w)]*:+.*$"
order: 0 order: 0
- title: 'Bug fixes' - title: "Bug fixes"
regexp: "^.*fix[(\\w)]*:+.*$" regexp: "^.*fix[(\\w)]*:+.*$"
order: 1 order: 1
- title: 'Enhancements' - title: "Enhancements"
regexp: "^.*chore[(\\w)]*:+.*$" regexp: "^.*chore[(\\w)]*:+.*$"
order: 2 order: 2
- title: "Refactor"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Build process updates"
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
order: 4
- title: "Documentation updates"
regexp: ^.*?docs?(\(.+\))??!?:.+$
order: 4
- title: Others - title: Others
order: 999 order: 999
filters:
# Commit messages matching the regexp listed here will be removed from
# the changelog
# Default is empty
exclude:
- '^docs'
- 'CICD'
- typo

View File

@ -1,5 +1,100 @@
# Gin ChangeLog # Gin ChangeLog
## Gin v1.10.0
### Features
* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
* feat(binding): support override default binding implement (#3514) (@ssfyn)
* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
### Bug fixes
* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
* fix(binding): dereference pointer to struct (#3199) (@echovl)
* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
* fix(engine): fix unit test (#3878) (@flc1125)
* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
* fix(uri): query binding bug (#3236) (@illiafox)
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
### Enhancements
* chore(CI): update release args (#3595) (@qloog)
* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
* chore(refactor): modify interface check way (#3855) (@demoManito)
* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
* chore: refactor CI and update dependencies (#3848) (@appleboy)
* chore: refactor configuration files for better readability (#3951) (@appleboy)
* chore: update GitHub Actions configuration (#3792) (@appleboy)
* chore: update changelog categories and improve documentation (#3917) (@appleboy)
* chore: update dependencies to latest versions (#3694) (@appleboy)
* chore: update external dependencies to latest versions (#3950) (@appleboy)
* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
### Build process updates
* build(codecov): Added a codecov configuration (#3891) (@flc1125)
* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
* ci(testing): add go1.22 version (#3842) (@appleboy)
### Documentation updates
* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
* docs: fix typo in comment (#3868) (@testwill)
* docs: fix typo in function documentation (#3872) (@TotomiEcio)
* docs: remove redundant comments (#3765) (@WeiTheShinobi)
* feat: update version constant to v1.10.0 (#3952) (@appleboy)
### Others
* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
* test(git): gitignore add develop tools (#3370) (@demoManito)
* test(http): use constant instead of numeric literal (#3863) (@testwill)
* test(path): Optimize unit test execution results (#3883) (@flc1125)
* test(render): increased unit tests coverage (#3691) (@araujo88)
## Gin v1.9.1
### BUG FIXES
* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
### SECURITY
* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
### ENHANCEMENTS
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
### DOCS
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
## Gin v1.9.0 ## Gin v1.9.0
### BREAK CHANGES ### BREAK CHANGES

View File

@ -42,6 +42,7 @@ fmt-check:
exit 1; \ exit 1; \
fi; fi;
.PHONY: vet
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)

25
auth.go
View File

@ -16,6 +16,9 @@ import (
// AuthUserKey is the cookie name for user credential in basic auth. // AuthUserKey is the cookie name for user credential in basic auth.
const AuthUserKey = "user" const AuthUserKey = "user"
// AuthProxyUserKey is the cookie name for proxy_user credential in basic auth for proxy.
const AuthProxyUserKey = "proxy_user"
// Accounts defines a key/value for user/pass list of authorized logins. // Accounts defines a key/value for user/pass list of authorized logins.
type Accounts map[string]string type Accounts map[string]string
@ -89,3 +92,25 @@ func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
} }
// BasicAuthForProxy returns a Basic HTTP Proxy-Authorization middleware.
// If the realm is empty, "Proxy Authorization Required" will be used by default.
func BasicAuthForProxy(accounts Accounts, realm string) HandlerFunc {
if realm == "" {
realm = "Proxy Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
pairs := processAccounts(accounts)
return func(c *Context) {
proxyUser, found := pairs.searchCredential(c.requestHeader("Proxy-Authorization"))
if !found {
// Credentials doesn't match, we return 407 and abort handlers chain.
c.Header("Proxy-Authenticate", realm)
c.AbortWithStatus(http.StatusProxyAuthRequired)
return
}
// The proxy_user credentials was found, set proxy_user's id to key AuthProxyUserKey in this context, the proxy_user's id can be read later using
// c.MustGet(gin.AuthProxyUserKey).
c.Set(AuthProxyUserKey, proxyUser)
}
}

View File

@ -137,3 +137,40 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
} }
func TestBasicAuthForProxySucceed(t *testing.T) {
accounts := Accounts{"admin": "password"}
router := New()
router.Use(BasicAuthForProxy(accounts, ""))
router.Any("/*proxyPath", func(c *Context) {
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String())
}
func TestBasicAuthForProxy407(t *testing.T) {
called := false
accounts := Accounts{"foo": "bar"}
router := New()
router.Use(BasicAuthForProxy(accounts, ""))
router.Any("/*proxyPath", func(c *Context) {
called = true
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
assert.False(t, called)
assert.Equal(t, http.StatusProxyAuthRequired, w.Code)
assert.Equal(t, "Basic realm=\"Proxy Authorization Required\"", w.Header().Get("Proxy-Authenticate"))
}

View File

@ -21,6 +21,7 @@ const (
MIMEMSGPACK = "application/x-msgpack" MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack" MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml" MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml" MIMETOML = "application/toml"
) )
@ -72,18 +73,18 @@ var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data // These implement the Binding interface and can be used to bind the data
// present in the request to struct instances. // present in the request to struct instances.
var ( var (
JSON = jsonBinding{} JSON BindingBody = jsonBinding{}
XML = xmlBinding{} XML BindingBody = xmlBinding{}
Form = formBinding{} Form Binding = formBinding{}
Query = queryBinding{} Query Binding = queryBinding{}
FormPost = formPostBinding{} FormPost Binding = formPostBinding{}
FormMultipart = formMultipartBinding{} FormMultipart Binding = formMultipartBinding{}
ProtoBuf = protobufBinding{} ProtoBuf BindingBody = protobufBinding{}
MsgPack = msgpackBinding{} MsgPack BindingBody = msgpackBinding{}
YAML = yamlBinding{} YAML BindingBody = yamlBinding{}
Uri = uriBinding{} Uri BindingUri = uriBinding{}
Header = headerBinding{} Header Binding = headerBinding{}
TOML = tomlBinding{} TOML BindingBody = tomlBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -102,7 +103,7 @@ func Default(method, contentType string) Binding {
return ProtoBuf return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2: case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack return MsgPack
case MIMEYAML: case MIMEYAML, MIMEYAML2:
return YAML return YAML
case MIMETOML: case MIMETOML:
return TOML return TOML

View File

@ -19,6 +19,7 @@ const (
MIMEMultipartPOSTForm = "multipart/form-data" MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf" MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml" MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml" MIMETOML = "application/toml"
) )
@ -96,7 +97,7 @@ func Default(method, contentType string) Binding {
return XML return XML
case MIMEPROTOBUF: case MIMEPROTOBUF:
return ProtoBuf return ProtoBuf
case MIMEYAML: case MIMEYAML, MIMEYAML2:
return YAML return YAML
case MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart return FormMultipart

View File

@ -164,6 +164,8 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, YAML, Default("POST", MIMEYAML)) assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML)) assert.Equal(t, YAML, Default("PUT", MIMEYAML))
assert.Equal(t, YAML, Default("POST", MIMEYAML2))
assert.Equal(t, YAML, Default("PUT", MIMEYAML2))
assert.Equal(t, TOML, Default("POST", MIMETOML)) assert.Equal(t, TOML, Default("POST", MIMETOML))
assert.Equal(t, TOML, Default("PUT", MIMETOML)) assert.Equal(t, TOML, Default("PUT", MIMETOML))

View File

@ -54,7 +54,10 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
value := reflect.ValueOf(obj) value := reflect.ValueOf(obj)
switch value.Kind() { switch value.Kind() {
case reflect.Ptr: case reflect.Ptr:
return v.ValidateStruct(value.Elem().Interface()) if value.Elem().Kind() != reflect.Struct {
return v.ValidateStruct(value.Elem().Interface())
}
return v.validateStruct(obj)
case reflect.Struct: case reflect.Struct:
return v.validateStruct(obj) return v.validateStruct(obj)
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:

View File

@ -7,6 +7,7 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -164,6 +165,23 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
return setter.TrySet(value, field, tagValue, setOpt) return setter.TrySet(value, field, tagValue, setOpt)
} }
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param.
UnmarshalParam(param string) error
}
// trySetCustom tries to set a custom type value
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
switch v := value.Addr().Interface().(type) {
case BindUnmarshaler:
return true, v.UnmarshalParam(val)
}
return false, nil
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) { func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue] vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists { if !ok && !opt.isDefaultExists {
@ -193,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if len(vs) > 0 { if len(vs) > 0 {
val = vs[0] val = vs[0]
} }
if ok, err := trySetCustom(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field) return true, setWithProperType(val, value, field)
} }
} }
@ -235,10 +256,17 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
switch value.Interface().(type) { switch value.Interface().(type) {
case time.Time: case time.Time:
return setTimeField(val, field, value) return setTimeField(val, field, value)
case multipart.FileHeader:
return nil
} }
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr:
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
return setWithProperType(val, value.Elem(), field)
default: default:
return errUnknownType return errUnknownType
} }
@ -369,11 +397,8 @@ func setTimeDuration(val string, value reflect.Value) error {
} }
func head(str, sep string) (head string, tail string) { func head(str, sep string) (head string, tail string) {
idx := strings.Index(str, sep) head, tail, _ = strings.Cut(str, sep)
if idx < 0 { return head, tail
return str, ""
}
return str[:idx], str[idx+len(sep):]
} }
func setFormMap(ptr any, form map[string][]string) error { func setFormMap(ptr any, form map[string][]string) error {

View File

@ -5,7 +5,11 @@
package binding package binding
import ( import (
"fmt"
"mime/multipart"
"reflect" "reflect"
"strconv"
"strings"
"testing" "testing"
"time" "time"
@ -43,6 +47,7 @@ func TestMappingBaseTypes(t *testing.T) {
{"zero value", struct{ F uint }{}, "", uint(0)}, {"zero value", struct{ F uint }{}, "", uint(0)},
{"zero value", struct{ F bool }{}, "", false}, {"zero value", struct{ F bool }{}, "", false},
{"zero value", struct{ F float32 }{}, "", float32(0)}, {"zero value", struct{ F float32 }{}, "", float32(0)},
{"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}},
} { } {
tp := reflect.TypeOf(tt.value) tp := reflect.TypeOf(tt.value)
testName := tt.name + ":" + tp.Field(0).Type.String() testName := tt.name + ":" + tp.Field(0).Type.String()
@ -269,6 +274,39 @@ func TestMappingStructField(t *testing.T) {
assert.Equal(t, 9, s.J.I) assert.Equal(t, 9, s.J.I)
} }
func TestMappingPtrField(t *testing.T) {
type ptrStruct struct {
Key int64 `json:"key"`
}
type ptrRequest struct {
Items []*ptrStruct `json:"items" form:"items"`
}
var err error
// With 0 items.
var req0 ptrRequest
err = mappingByPtr(&req0, formSource{}, "form")
assert.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)
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)
assert.Len(t, req2.Items, 2)
assert.EqualValues(t, 1, req2.Items[0].Key)
assert.EqualValues(t, 2, req2.Items[1].Key)
}
func TestMappingMapField(t *testing.T) { func TestMappingMapField(t *testing.T) {
var s struct { var s struct {
M map[string]int M map[string]int
@ -288,3 +326,99 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err) assert.NoError(t, err)
} }
type customUnmarshalParamHex int
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
v, err := strconv.ParseInt(param, 16, 64)
if err != nil {
return err
}
*f = customUnmarshalParamHex(v)
return nil
}
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `form:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
assert.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `uri:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
assert.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
type customUnmarshalParamType struct {
Protocol string
Path string
Name string
}
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
parts := strings.Split(param, ":")
if len(parts) != 3 {
return fmt.Errorf("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
f.Name = parts[2]
return nil
}
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}

View File

@ -192,6 +192,30 @@ func TestValidatePrimitives(t *testing.T) {
assert.Equal(t, "value", str) assert.Equal(t, "value", str)
} }
type structModifyValidation struct {
Integer int
}
func toZero(sl validator.StructLevel) {
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
s.Integer = 0
}
func TestValidateAndModifyStruct(t *testing.T) {
// This validates that pointers to structs are passed to the validator
// giving us the ability to modify the struct being validated.
engine, ok := Validator.Engine().(*validator.Validate)
assert.True(t, ok)
engine.RegisterStructValidation(toZero, structModifyValidation{})
s := structModifyValidation{Integer: 1}
errs := validate(&s)
assert.Nil(t, errs)
assert.Equal(t, s, structModifyValidation{Integer: 0})
}
// structCustomValidation is a helper struct we use to check that // structCustomValidation is a helper struct we use to check that
// custom validation can be registered on it. // custom validation can be registered on it.
// The `notone` binding directive is for custom validation and registered later. // The `notone` binding directive is for custom validation and registered later.

13
codecov.yml Normal file
View File

@ -0,0 +1,13 @@
coverage:
require_ci_to_pass: true
status:
project:
default:
target: 99%
threshold: 99%
patch:
default:
target: 99%
threshold: 95%

View File

@ -43,6 +43,10 @@ const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
// ContextKey is the key that a Context returns itself for. // ContextKey is the key that a Context returns itself for.
const ContextKey = "_gin-gonic/gin/contextkey" const ContextKey = "_gin-gonic/gin/contextkey"
type ContextKeyType int
const ContextRequestKey ContextKeyType = 0
// abortIndex represents a typical value used in abort functions. // abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 const abortIndex int8 = math.MaxInt8 >> 1
@ -113,20 +117,27 @@ func (c *Context) Copy() *Context {
cp := Context{ cp := Context{
writermem: c.writermem, writermem: c.writermem,
Request: c.Request, Request: c.Request,
Params: c.Params,
engine: c.engine, engine: c.engine,
} }
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = abortIndex cp.index = abortIndex
cp.handlers = nil cp.handlers = nil
cp.Keys = map[string]any{} cp.fullPath = c.fullPath
for k, v := range c.Keys {
cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
c.mu.RLock()
for k, v := range cKeys {
cp.Keys[k] = v cp.Keys[k] = v
} }
paramCopy := make([]Param, len(cp.Params)) c.mu.RUnlock()
copy(paramCopy, cp.Params)
cp.Params = paramCopy cParams := c.Params
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
return &cp return &cp
} }
@ -386,7 +397,7 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
// //
// router.GET("/user/:id", func(c *gin.Context) { // router.GET("/user/:id", func(c *gin.Context) {
// // a GET request to /user/john // // a GET request to /user/john
// id := c.Param("id") // id == "/john" // id := c.Param("id") // id == "john"
// // a GET request to /user/john/ // // a GET request to /user/john/
// id := c.Param("id") // id == "/john/" // id := c.Param("id") // id == "/john/"
// }) // })
@ -728,7 +739,7 @@ func (c *Context) ShouldBindHeader(obj any) error {
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj any) error { func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string) m := make(map[string][]string, len(c.Params))
for _, v := range c.Params { for _, v := range c.Params {
m[v.Key] = []string{v.Value} m[v.Key] = []string{v.Value}
} }
@ -763,6 +774,26 @@ func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error
return bb.BindBody(body, obj) return bb.BindBody(body, obj)
} }
// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
func (c *Context) ShouldBindBodyWithJSON(obj any) error {
return c.ShouldBindBodyWith(obj, binding.JSON)
}
// ShouldBindBodyWithXML is a shortcut for c.ShouldBindBodyWith(obj, binding.XML).
func (c *Context) ShouldBindBodyWithXML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.XML)
}
// ShouldBindBodyWithYAML is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML).
func (c *Context) ShouldBindBodyWithYAML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.YAML)
}
// ShouldBindBodyWithTOML is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML).
func (c *Context) ShouldBindBodyWithTOML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.TOML)
}
// ClientIP implements one best effort algorithm to return the real client IP. // 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. // 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]).
@ -873,6 +904,9 @@ func (c *Context) GetHeader(key string) string {
// GetRawData returns stream data. // GetRawData returns stream data.
func (c *Context) GetRawData() ([]byte, error) { func (c *Context) GetRawData() ([]byte, error) {
if c.Request.Body == nil {
return nil, errors.New("cannot read nil body")
}
return io.ReadAll(c.Request.Body) return io.ReadAll(c.Request.Body)
} }
@ -1052,11 +1086,17 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
http.FileServer(fs).ServeHTTP(c.Writer, c.Request) http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
} }
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
// FileAttachment writes the specified file into the body stream in an efficient way // FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename // On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) { func (c *Context) FileAttachment(filepath, filename string) {
if isASCII(filename) { if isASCII(filename) {
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+escapeQuotes(filename)+`"`)
} else { } else {
c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename))
} }
@ -1174,9 +1214,16 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/ /************************************/
// hasRequestContext returns whether c.Request has Context and fallback.
func (c *Context) hasRequestContext() bool {
hasFallback := c.engine != nil && c.engine.ContextWithFallback
hasRequestContext := c.Request != nil && c.Request.Context() != nil
return hasFallback && hasRequestContext
}
// Deadline returns that there is no deadline (ok==false) when c.Request has no Context. // Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
func (c *Context) Deadline() (deadline time.Time, ok bool) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { if !c.hasRequestContext() {
return return
} }
return c.Request.Context().Deadline() return c.Request.Context().Deadline()
@ -1184,7 +1231,7 @@ func (c *Context) Deadline() (deadline time.Time, ok bool) {
// Done returns nil (chan which will wait forever) when c.Request has no Context. // Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { if !c.hasRequestContext() {
return nil return nil
} }
return c.Request.Context().Done() return c.Request.Context().Done()
@ -1192,7 +1239,7 @@ func (c *Context) Done() <-chan struct{} {
// Err returns nil when c.Request has no Context. // Err returns nil when c.Request has no Context.
func (c *Context) Err() error { func (c *Context) Err() error {
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { if !c.hasRequestContext() {
return nil return nil
} }
return c.Request.Context().Err() return c.Request.Context().Err()
@ -1202,7 +1249,7 @@ func (c *Context) Err() error {
// if no value is associated with key. Successive calls to Value with // if no value is associated with key. Successive calls to Value with
// the same key returns the same result. // the same key returns the same result.
func (c *Context) Value(key any) any { func (c *Context) Value(key any) any {
if key == 0 { if key == ContextRequestKey {
return c.Request return c.Request
} }
if key == ContextKey { if key == ContextKey {
@ -1213,7 +1260,7 @@ func (c *Context) Value(key any) any {
return val return val
} }
} }
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { if !c.hasRequestContext() {
return nil return nil
} }
return c.Request.Context().Value(key) return c.Request.Context().Value(key)

View File

@ -324,6 +324,7 @@ func TestContextCopy(t *testing.T) {
c.handlers = HandlersChain{func(c *Context) {}} c.handlers = HandlersChain{func(c *Context) {}}
c.Params = Params{Param{Key: "foo", Value: "bar"}} c.Params = Params{Param{Key: "foo", Value: "bar"}}
c.Set("foo", "bar") c.Set("foo", "bar")
c.fullPath = "/hola"
cp := c.Copy() cp := c.Copy()
assert.Nil(t, cp.handlers) assert.Nil(t, cp.handlers)
@ -336,6 +337,7 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.Params, c.Params) assert.Equal(t, cp.Params, c.Params)
cp.Set("foo", "notBar") cp.Set("foo", "notBar")
assert.False(t, cp.Keys["foo"] == c.Keys["foo"]) assert.False(t, cp.Keys["foo"] == c.Keys["foo"])
assert.Equal(t, cp.fullPath, c.fullPath)
} }
func TestContextHandlerName(t *testing.T) { func TestContextHandlerName(t *testing.T) {
@ -998,7 +1000,7 @@ func TestContextRenderFile(t *testing.T) {
c.File("./gin.go") c.File("./gin.go")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16, // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8' // else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.NotEqual(t, "", w.Header().Get("Content-Type"))
@ -1012,7 +1014,7 @@ func TestContextRenderFileFromFS(t *testing.T) {
c.FileFromFS("./gin.go", Dir(".", false)) c.FileFromFS("./gin.go", Dir(".", false))
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16, // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8' // else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.NotEqual(t, "", w.Header().Get("Content-Type"))
@ -1028,10 +1030,24 @@ func TestContextRenderAttachment(t *testing.T) {
c.FileAttachment("./gin.go", newFilename) c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition")) assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
} }
func TestContextRenderAndEscapeAttachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
maliciousFilename := "tampering_field.sh\"; \\\"; dummy=.go"
actualEscapedResponseFilename := "tampering_field.sh\\\"; \\\\\\\"; dummy=.go"
c.Request, _ = http.NewRequest("GET", "/", nil)
c.FileAttachment("./gin.go", maliciousFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", actualEscapedResponseFilename), w.Header().Get("Content-Disposition"))
}
func TestContextRenderUTF8Attachment(t *testing.T) { func TestContextRenderUTF8Attachment(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1041,12 +1057,12 @@ func TestContextRenderUTF8Attachment(t *testing.T) {
c.FileAttachment("./gin.go", newFilename) c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {") assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition")) assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition"))
} }
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml // and Content-Type is set to application/yaml
func TestContextRenderYAML(t *testing.T) { func TestContextRenderYAML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1055,7 +1071,7 @@ func TestContextRenderYAML(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String()) assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
// TestContextRenderTOML tests that the response is serialized as TOML // TestContextRenderTOML tests that the response is serialized as TOML
@ -1203,7 +1219,7 @@ func TestContextNegotiationWithYAML(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String()) assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextNegotiationWithTOML(t *testing.T) { func TestContextNegotiationWithTOML(t *testing.T) {
@ -1555,6 +1571,12 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Del("CF-Connecting-IP") c.Request.Header.Del("CF-Connecting-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP()) assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = PlatformFlyIO
assert.Equal(t, "70.70.70.70", c.ClientIP())
c.Request.Header.Del("Fly-Client-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = "" c.engine.TrustedPlatform = ""
// no port // no port
@ -1567,6 +1589,7 @@ func resetContextForClientIPTests(c *Context) {
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50") c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60") c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
c.Request.Header.Set("Fly-Client-IP", "70.70.70.70")
c.Request.RemoteAddr = " 40.40.40.40:42123 " c.Request.RemoteAddr = " 40.40.40.40:42123 "
c.engine.TrustedPlatform = "" c.engine.TrustedPlatform = ""
c.engine.trustedCIDRs = defaultTrustedCIDRs c.engine.trustedCIDRs = defaultTrustedCIDRs
@ -1954,6 +1977,263 @@ func TestContextShouldBindBodyWith(t *testing.T) {
} }
} }
func TestContextShouldBindBodyWithJSON(t *testing.T) {
for _, tt := range []struct {
name string
bindingBody binding.BindingBody
body string
}{
{
name: " JSON & JSON-BODY ",
bindingBody: binding.JSON,
body: `{"foo":"FOO"}`,
},
{
name: " JSON & XML-BODY ",
bindingBody: binding.XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: " JSON & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " JSON & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
} {
t.Logf("testing: %s", tt.name)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(tt.body))
type typeJSON struct {
Foo string `json:"foo" binding:"required"`
}
objJSON := typeJSON{}
if tt.bindingBody == binding.JSON {
assert.NoError(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{"FOO"}, objJSON)
}
if tt.bindingBody == binding.XML {
assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
if tt.bindingBody == binding.YAML {
assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
if tt.bindingBody == binding.TOML {
assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
}
}
func TestContextShouldBindBodyWithXML(t *testing.T) {
for _, tt := range []struct {
name string
bindingBody binding.BindingBody
body string
}{
{
name: " XML & JSON-BODY ",
bindingBody: binding.JSON,
body: `{"foo":"FOO"}`,
},
{
name: " XML & XML-BODY ",
bindingBody: binding.XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: " XML & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " XML & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
} {
t.Logf("testing: %s", tt.name)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(tt.body))
type typeXML struct {
Foo string `xml:"foo" binding:"required"`
}
objXML := typeXML{}
if tt.bindingBody == binding.JSON {
assert.Error(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{}, objXML)
}
if tt.bindingBody == binding.XML {
assert.NoError(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{"FOO"}, objXML)
}
if tt.bindingBody == binding.YAML {
assert.Error(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{}, objXML)
}
if tt.bindingBody == binding.TOML {
assert.Error(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{}, objXML)
}
}
}
func TestContextShouldBindBodyWithYAML(t *testing.T) {
for _, tt := range []struct {
name string
bindingBody binding.BindingBody
body string
}{
{
name: " YAML & JSON-BODY ",
bindingBody: binding.JSON,
body: `{"foo":"FOO"}`,
},
{
name: " YAML & XML-BODY ",
bindingBody: binding.XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: " YAML & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " YAML & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
} {
t.Logf("testing: %s", tt.name)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(tt.body))
type typeYAML struct {
Foo string `yaml:"foo" binding:"required"`
}
objYAML := typeYAML{}
// YAML belongs to a super collection of JSON, so JSON can be parsed by YAML
if tt.bindingBody == binding.JSON {
assert.NoError(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{"FOO"}, objYAML)
}
if tt.bindingBody == binding.XML {
assert.Error(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{}, objYAML)
}
if tt.bindingBody == binding.YAML {
assert.NoError(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{"FOO"}, objYAML)
}
if tt.bindingBody == binding.TOML {
assert.Error(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{}, objYAML)
}
}
}
func TestContextShouldBindBodyWithTOML(t *testing.T) {
for _, tt := range []struct {
name string
bindingBody binding.BindingBody
body string
}{
{
name: " TOML & JSON-BODY ",
bindingBody: binding.JSON,
body: `{"foo":"FOO"}`,
},
{
name: " TOML & XML-BODY ",
bindingBody: binding.XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: " TOML & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " TOML & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo = 'FOO'`,
},
} {
t.Logf("testing: %s", tt.name)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(tt.body))
type typeTOML struct {
Foo string `toml:"foo" binding:"required"`
}
objTOML := typeTOML{}
if tt.bindingBody == binding.JSON {
assert.Error(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{}, objTOML)
}
if tt.bindingBody == binding.XML {
assert.Error(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{}, objTOML)
}
if tt.bindingBody == binding.YAML {
assert.Error(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{}, objTOML)
}
if tt.bindingBody == binding.TOML {
assert.NoError(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{"FOO"}, objTOML)
}
}
}
func TestContextGolangContext(t *testing.T) { func TestContextGolangContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
@ -1962,7 +2242,7 @@ func TestContextGolangContext(t *testing.T) {
ti, ok := c.Deadline() ti, ok := c.Deadline()
assert.Equal(t, ti, time.Time{}) assert.Equal(t, ti, time.Time{})
assert.False(t, ok) assert.False(t, ok)
assert.Equal(t, c.Value(0), c.Request) assert.Equal(t, c.Value(ContextRequestKey), c.Request)
assert.Equal(t, c.Value(ContextKey), c) assert.Equal(t, c.Value(ContextKey), c)
assert.Nil(t, c.Value("foo")) assert.Nil(t, c.Value("foo"))
@ -2162,6 +2442,24 @@ func TestRemoteIPFail(t *testing.T) {
assert.False(t, trust) assert.False(t, trust)
} }
func TestHasRequestContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.hasRequestContext(), "no request, no fallback")
c.engine.ContextWithFallback = true
assert.False(t, c.hasRequestContext(), "no request, has fallback")
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
assert.True(t, c.hasRequestContext(), "has request, has fallback")
c.Request, _ = http.NewRequestWithContext(nil, "", "", nil) //nolint:staticcheck
assert.False(t, c.hasRequestContext(), "has request with nil ctx, has fallback")
c.engine.ContextWithFallback = false
assert.False(t, c.hasRequestContext(), "has request, no fallback")
c = &Context{}
assert.False(t, c.hasRequestContext(), "no request, no engine")
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
assert.False(t, c.hasRequestContext(), "has request, no engine")
}
func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) { func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag // enable ContextWithFallback feature flag

View File

@ -23,6 +23,9 @@ func IsDebugging() bool {
// DebugPrintRouteFunc indicates debug log output format. // DebugPrintRouteFunc indicates debug log output format.
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int) var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
// DebugPrintFunc indicates debug log output format.
var DebugPrintFunc func(format string, values ...interface{})
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
@ -48,12 +51,19 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
} }
func debugPrint(format string, values ...any) { func debugPrint(format string, values ...any) {
if IsDebugging() { if !IsDebugging() {
if !strings.HasSuffix(format, "\n") { return
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
} }
if DebugPrintFunc != nil {
DebugPrintFunc(format, values...)
return
}
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
} }
func getMinVer(v string) (uint64, error) { func getMinVer(v string) (uint64, error) {

View File

@ -12,6 +12,8 @@ import (
// BindWith binds the passed struct pointer using the specified binding engine. // BindWith binds the passed struct pointer using the specified binding engine.
// See the binding package. // See the binding package.
//
// Deprecated: Use MustBindWith or ShouldBindWith.
func (c *Context) BindWith(obj any, b binding.Binding) error { func (c *Context) BindWith(obj any, b binding.Binding) error {
log.Println(`BindWith(\"any, binding.Binding\") error is going to log.Println(`BindWith(\"any, binding.Binding\") error is going to
be deprecated, please check issue #662 and either use MustBindWith() if you be deprecated, please check issue #662 and either use MustBindWith() if you

View File

@ -27,6 +27,7 @@
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri) - [Bind Uri](#bind-uri)
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
- [Bind Header](#bind-header) - [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
@ -508,6 +509,44 @@ Sample Output
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
``` ```
### Skip logging
```go
func main() {
router := gin.New()
// skip logging for desired paths by setting SkipPaths in LoggerConfig
loggerConfig := gin.LoggerConfig{SkipPaths: []string{"/metrics"}}
// skip logging based on your logic by setting Skip func in LoggerConfig
loggerConfig.Skip = func(c *gin.Context) bool {
// as an example skip non server side errors
return c.Writer.Status() < http.StatusInternalServerError
}
engine.Use(gin.LoggerWithConfig(loggerConfig))
router.Use(gin.Recovery())
// skipped
router.GET("/metrics", func(c *gin.Context) {
c.Status(http.StatusNotImplemented)
})
// skipped
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// not skipped
router.GET("/data", func(c *gin.Context) {
c.Status(http.StatusNotImplemented)
})
router.Run(":8080")
}
```
### Controlling Log output coloring ### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY. By default, logs output on console should be colorized depending on the detected TTY.
@ -861,6 +900,46 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
curl -v localhost:8088/thinkerou/not-uuid curl -v localhost:8088/thinkerou/not-uuid
``` ```
### Bind custom unmarshaler
```go
package main
import (
"github.com/gin-gonic/gin"
"strings"
)
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1))
return nil
}
func main() {
route := gin.Default()
var request struct {
Birthday Birthday `form:"birthday"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
route.Run(":8088")
}
```
Test it with:
```sh
curl 'localhost:8088/test?birthday=2000-01-01'
```
Result
```sh
"2000/01/01"
```
### Bind Header ### Bind Header
```go ```go
@ -1918,7 +1997,12 @@ func SomeHandler(c *gin.Context) {
} }
``` ```
For this, you can use `c.ShouldBindBodyWith`. For this, you can use `c.ShouldBindBodyWith` or shortcuts.
- `c.ShouldBindBodyWithJSON` is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
- `c.ShouldBindBodyWithXML` is a shortcut for c.ShouldBindBodyWith(obj, binding.XML).
- `c.ShouldBindBodyWithYAML` is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML).
- `c.ShouldBindBodyWithTOML` is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML).
```go ```go
func SomeHandler(c *gin.Context) { func SomeHandler(c *gin.Context) {
@ -1931,7 +2015,7 @@ func SomeHandler(c *gin.Context) {
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil { } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`) c.String(http.StatusOK, `the body should be formB JSON`)
// And it can accepts other formats // And it can accepts other formats
} else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil { } else if errB2 := c.ShouldBindBodyWithXML(&objB); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`) c.String(http.StatusOK, `the body should be formB XML`)
} else { } else {
... ...
@ -2176,10 +2260,16 @@ import (
func main() { func main() {
router := gin.Default() router := gin.Default()
// Use predefined header gin.PlatformXXX // Use predefined header gin.PlatformXXX
// Google App Engine
router.TrustedPlatform = gin.PlatformGoogleAppEngine router.TrustedPlatform = gin.PlatformGoogleAppEngine
// Or set your own trusted request header for another trusted proxy service // Cloudflare
// Don't set it to any suspect request header, it's unsafe router.TrustedPlatform = gin.PlatformCloudflare
router.TrustedPlatform = "X-CDN-IP" // Fly.io
router.TrustedPlatform = gin.PlatformFlyIO
// Or, you can set your own trusted request header. But be sure your CDN
// prevents users from passing this header! For example, if your CDN puts
// the client IP in X-CDN-Client-IP:
router.TrustedPlatform = "X-CDN-Client-IP"
router.GET("/", func(c *gin.Context) { router.GET("/", func(c *gin.Context) {
// If you set TrustedPlatform, ClientIP() will resolve the // If you set TrustedPlatform, ClientIP() will resolve the

66
gin.go
View File

@ -47,6 +47,9 @@ var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
// HandlerFunc defines the handler used by gin middleware as return value. // HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
// OptionFunc defines the function to change the default configuration
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice. // HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
@ -77,6 +80,8 @@ const (
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining // PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP // the client's IP
PlatformCloudflare = "CF-Connecting-IP" PlatformCloudflare = "CF-Connecting-IP"
// PlatformFlyIO when running on Fly.io. Trust Fly-Client-IP for determining the client's IP
PlatformFlyIO = "Fly-Client-IP"
) )
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
@ -180,7 +185,7 @@ var _ IRouter = (*Engine)(nil)
// - ForwardedByClientIP: true // - ForwardedByClientIP: true
// - UseRawPath: false // - UseRawPath: false
// - UnescapePathValues: true // - UnescapePathValues: true
func New() *Engine { func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
engine := &Engine{ engine := &Engine{
RouterGroup: RouterGroup{ RouterGroup: RouterGroup{
@ -209,15 +214,15 @@ func New() *Engine {
engine.pool.New = func() any { engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams) return engine.allocateContext(engine.maxParams)
} }
return engine return engine.With(opts...)
} }
// Default returns an Engine instance with the Logger and Recovery middleware already attached. // Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine { func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
engine := New() engine := New()
engine.Use(Logger(), Recovery()) engine.Use(Logger(), Recovery())
return engine return engine.With(opts...)
} }
func (engine *Engine) Handler() http.Handler { func (engine *Engine) Handler() http.Handler {
@ -311,6 +316,15 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine return engine
} }
// With returns a Engine with the configuration set in the OptionFunc.
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
}
return engine
}
func (engine *Engine) rebuild404Handlers() { func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute) engine.allNoRoute = engine.combineHandlers(engine.noRoute)
} }
@ -334,7 +348,6 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
} }
root.addRoute(path, handlers) root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams { if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount engine.maxParams = paramsCount
} }
@ -370,6 +383,24 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
return routes return routes
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
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.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) { func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.trustedProxies == nil { if engine.trustedProxies == nil {
return nil, nil return nil, nil
@ -473,7 +504,14 @@ func parseIP(ip string) net.IP {
return parsedIP return parsedIP
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests. //
attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router) // It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) { func (engine *Engine) Run(addr ...string) (err error) {
@ -499,7 +537,7 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + 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 = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler()) err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
@ -634,17 +672,25 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
if engine.HandleMethodNotAllowed { if engine.HandleMethodNotAllowed {
// 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)
for _, tree := range engine.trees { for _, tree := range engine.trees {
if tree.method == httpMethod { if tree.method == httpMethod {
continue continue
} }
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil { if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod allowed = append(allowed, tree.method)
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
} }
} }
if len(allowed) > 0 {
c.handlers = engine.allNoMethod
c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
} }
c.handlers = engine.allNoRoute c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body) serveError(c, http.StatusNotFound, default404Body)
} }

View File

@ -14,6 +14,7 @@ import (
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@ -696,3 +697,60 @@ func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo)
func handlerTest1(c *Context) {} func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {} func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
}
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"})
}
func TestWithOptionFunc(t *testing.T) {
r := New()
r.With(func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
})
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"})
}
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1))
return nil
}
func TestCustomUnmarshalStruct(t *testing.T) {
route := Default()
var request struct {
Birthday Birthday `form:"birthday"`
}
route.GET("/test", func(ctx *Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
req := httptest.NewRequest("GET", "/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())
}

36
go.mod
View File

@ -3,24 +3,26 @@ module github.com/gin-gonic/gin
go 1.20 go 1.20
require ( require (
github.com/bytedance/sonic v1.8.8 github.com/bytedance/sonic v1.11.6
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.12.0 github.com/go-playground/validator/v10 v10.20.0
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.18 github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.0.7 github.com/pelletier/go-toml/v2 v2.2.2
github.com/quic-go/quic-go v0.34.0 github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.8.2 github.com/ugorji/go/codec v1.2.12
github.com/ugorji/go/codec v1.2.11 golang.org/x/net v0.25.0
golang.org/x/net v0.9.0 google.golang.org/protobuf v1.34.1
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 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/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
@ -30,6 +32,9 @@ require (
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.2 // indirect github.com/leodido/go-urn v1.2.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
@ -37,11 +42,8 @@ require (
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.7.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/text v0.15.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.6.0 // indirect
) )

9
go.sum
View File

@ -11,6 +11,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -72,14 +74,16 @@ github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2u
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/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.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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 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/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
@ -135,4 +139,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -47,8 +47,15 @@ type LoggerConfig struct {
// SkipPaths is an url path array which logs are not written. // SkipPaths is an url path array which logs are not written.
// Optional. // Optional.
SkipPaths []string SkipPaths []string
// Skip is a Skipper that indicates which logs should not be written.
// Optional.
Skip Skipper
} }
// Skipper is a function to skip logs based on provided Context
type Skipper func(c *Context) bool
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter // LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
type LogFormatter func(params LogFormatterParams) string type LogFormatter func(params LogFormatterParams) string
@ -83,6 +90,8 @@ func (p *LogFormatterParams) StatusCodeColor() string {
code := p.StatusCode code := p.StatusCode
switch { switch {
case code >= http.StatusContinue && code < http.StatusOK:
return white
case code >= http.StatusOK && code < http.StatusMultipleChoices: case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green return green
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
@ -239,32 +248,34 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
// Process request // Process request
c.Next() c.Next()
// Log only when path is not being skipped // Log only when it is not being skipped
if _, ok := skip[path]; !ok { if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) {
param := LogFormatterParams{ return
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
fmt.Fprint(out, formatter(param))
} }
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
fmt.Fprint(out, formatter(param))
} }
} }

View File

@ -310,6 +310,7 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor() return p.StatusCodeColor()
} }
assert.Equal(t, white, colorForStatus(http.StatusContinue), "1xx should be white")
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
@ -414,6 +415,26 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
func TestLoggerWithConfigSkipper(t *testing.T) {
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Skip: func(c *Context) bool {
return c.Writer.Status() == http.StatusNoContent
},
}))
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestDisableConsoleColor(t *testing.T) { func TestDisableConsoleColor(t *testing.T) {
New() New()
assert.Equal(t, autoColor, consoleColorMode) assert.Equal(t, autoColor, consoleColorMode)

View File

@ -6,6 +6,7 @@
package gin package gin
import ( import (
"runtime"
"strings" "strings"
"testing" "testing"
@ -80,6 +81,10 @@ func TestPathCleanMallocs(t *testing.T) {
t.Skip("skipping malloc count in short mode") t.Skip("skipping malloc count in short mode")
} }
if runtime.GOMAXPROCS(0) > 1 {
t.Skip("skipping malloc count; GOMAXPROCS>1")
}
for _, test := range cleanTests { for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) }) allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.EqualValues(t, allocs, 0) assert.EqualValues(t, allocs, 0)

View File

@ -15,22 +15,22 @@ type Render interface {
} }
var ( var (
_ Render = JSON{} _ Render = (*JSON)(nil)
_ Render = IndentedJSON{} _ Render = (*IndentedJSON)(nil)
_ Render = SecureJSON{} _ Render = (*SecureJSON)(nil)
_ Render = JsonpJSON{} _ Render = (*JsonpJSON)(nil)
_ Render = XML{} _ Render = (*XML)(nil)
_ Render = String{} _ Render = (*String)(nil)
_ Render = Redirect{} _ Render = (*Redirect)(nil)
_ Render = Data{} _ Render = (*Data)(nil)
_ Render = HTML{} _ Render = (*HTML)(nil)
_ HTMLRender = HTMLDebug{} _ HTMLRender = (*HTMLDebug)(nil)
_ HTMLRender = HTMLProduction{} _ HTMLRender = (*HTMLProduction)(nil)
_ Render = YAML{} _ Render = (*YAML)(nil)
_ Render = Reader{} _ Render = (*Reader)(nil)
_ Render = AsciiJSON{} _ Render = (*AsciiJSON)(nil)
_ Render = ProtoBuf{} _ Render = (*ProtoBuf)(nil)
_ Render = TOML{} _ Render = (*TOML)(nil)
) )
func writeContentType(w http.ResponseWriter, value []string) { func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -15,6 +15,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gin-gonic/gin/internal/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -136,6 +137,51 @@ func TestRenderJsonpJSON(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
} }
type errorWriter struct {
bufString string
*httptest.ResponseRecorder
}
var _ http.ResponseWriter = (*errorWriter)(nil)
func (w *errorWriter) Write(buf []byte) (int, error) {
if string(buf) == w.bufString {
return 0, errors.New(`write "` + w.bufString + `" error`)
}
return w.ResponseRecorder.Write(buf)
}
func TestRenderJsonpJSONError(t *testing.T) {
ew := &errorWriter{
ResponseRecorder: httptest.NewRecorder(),
}
jsonpJSON := JsonpJSON{
Callback: "foo",
Data: map[string]string{
"foo": "bar",
},
}
cb := template.JSEscapeString(jsonpJSON.Callback)
ew.bufString = cb
err := jsonpJSON.Render(ew) // error was returned while writing callback
assert.Equal(t, `write "`+cb+`" error`, err.Error())
ew.bufString = `(`
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
ew.bufString = string(data)
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
ew.bufString = `);`
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`);`+`" error`, err.Error())
}
func TestRenderJsonpJSONError2(t *testing.T) { func TestRenderJsonpJSONError2(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]any{ data := map[string]any{
@ -234,12 +280,12 @@ b:
d: [3, 4] d: [3, 4]
` `
(YAML{data}).WriteContentType(w) (YAML{data}).WriteContentType(w)
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w) err := (YAML{data}).Render(w)
assert.NoError(t, err) assert.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, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
type fail struct{} type fail struct{}
@ -532,3 +578,16 @@ func TestRenderReaderNoContentLength(t *testing.T) {
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
} }
func TestRenderWriteError(t *testing.T) {
data := []interface{}{"value1", "value2"}
prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{
bufString: prefix,
ResponseRecorder: httptest.NewRecorder(),
}
err := r.Render(ew)
assert.NotNil(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error())
}

View File

@ -15,7 +15,7 @@ type YAML struct {
Data any Data any
} }
var yamlContentType = []string{"application/x-yaml; charset=utf-8"} var yamlContentType = []string{"application/yaml; charset=utf-8"}
// Render (YAML) marshals the given interface object and writes data with custom ContentType. // Render (YAML) marshals the given interface object and writes data with custom ContentType.
func (r YAML) Render(w http.ResponseWriter) error { func (r YAML) Render(w http.ResponseWriter) error {

View File

@ -156,3 +156,33 @@ func TestResponseWriterStatusCode(t *testing.T) {
// status must be 200 although we tried to change it // status must be 200 although we tried to change it
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
} }
// mockPusherResponseWriter is an http.ResponseWriter that implements http.Pusher.
type mockPusherResponseWriter struct {
http.ResponseWriter
}
func (m *mockPusherResponseWriter) Push(target string, opts *http.PushOptions) error {
return nil
}
// nonPusherResponseWriter is an http.ResponseWriter that does not implement http.Pusher.
type nonPusherResponseWriter struct {
http.ResponseWriter
}
func TestPusherWithPusher(t *testing.T) {
rw := &mockPusherResponseWriter{}
w := &responseWriter{ResponseWriter: rw}
pusher := w.Pusher()
assert.NotNil(t, pusher, "Expected pusher to be non-nil")
}
func TestPusherWithoutPusher(t *testing.T) {
rw := &nonPusherResponseWriter{}
w := &responseWriter{ResponseWriter: rw}
pusher := w.Pusher()
assert.Nil(t, pusher, "Expected pusher to be nil")
}

View File

@ -180,58 +180,58 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, 200, w.Code) assert.Equal(t, http.StatusOK, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"})
assert.Equal(t, "/api/path", w.Header().Get("Location")) assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path", w.Header().Get("Location")) assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"})
assert.Equal(t, "//path", w.Header().Get("Location")) assert.Equal(t, "//path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"})
assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location")) assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location")) assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"})
assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location")) assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"})
assert.Equal(t, "api/api/path", w.Header().Get("Location")) assert.Equal(t, "api/api/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"})
assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location")) assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"}) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"})
assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location")) assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code) assert.Equal(t, http.StatusMovedPermanently, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
@ -337,6 +337,45 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.Equal(t, "/is/super/great", wild) assert.Equal(t, "/is/super/great", wild)
} }
// TestRouteParamsNotEmpty tests that context parameters will be set
// even if a route with params/wildcards is registered after the context
// initialisation (which happened in a previous requests).
func TestRouteParamsNotEmpty(t *testing.T) {
name := ""
lastName := ""
wild := ""
router := New()
w := PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusNotFound, w.Code)
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name")
var ok bool
wild, ok = c.Params.Get("wild")
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
assert.Empty(t, c.Params.ByName("wtf"))
wtf, ok := c.Params.Get("wtf")
assert.Empty(t, wtf)
assert.False(t, ok)
})
w = PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
assert.Equal(t, "smith", lastName)
assert.Equal(t, "/is/super/great", wild)
}
// TestHandleStaticFile - ensure the static file handles properly // TestHandleStaticFile - ensure the static file handles properly
func TestRouteStaticFile(t *testing.T) { func TestRouteStaticFile(t *testing.T) {
// SETUP file // SETUP file
@ -475,6 +514,18 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
func TestRouteNotAllowedEnabled3(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
router.GET("/path", func(c *Context) {})
router.POST("/path", func(c *Context) {})
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")
}
func TestRouteNotAllowedDisabled(t *testing.T) { func TestRouteNotAllowedDisabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
@ -568,11 +619,11 @@ func TestRouterNotFound(t *testing.T) {
router = New() router = New()
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
if c.Request.RequestURI == "/login" { if c.Request.RequestURI == "/login" {
c.String(200, "login") c.String(http.StatusOK, "login")
} }
}) })
router.GET("/logout", func(c *Context) { router.GET("/logout", func(c *Context) {
c.String(200, "logout") c.String(http.StatusOK, "logout")
}) })
w = PerformRequest(router, http.MethodGet, "/login") w = PerformRequest(router, http.MethodGet, "/login")
assert.Equal(t, "login", w.Body.String()) assert.Equal(t, "login", w.Body.String())
@ -584,7 +635,7 @@ func TestRouterStaticFSNotFound(t *testing.T) {
router := New() router := New()
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
c.String(404, "non existent") c.String(http.StatusNotFound, "non existent")
}) })
w := PerformRequest(router, http.MethodGet, "/nonexistent") w := PerformRequest(router, http.MethodGet, "/nonexistent")
@ -667,12 +718,12 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
func TestRouteServeErrorWithWriteHeader(t *testing.T) { func TestRouteServeErrorWithWriteHeader(t *testing.T) {
route := New() route := New()
route.Use(func(c *Context) { route.Use(func(c *Context) {
c.Status(421) c.Status(http.StatusMisdirectedRequest)
c.Next() c.Next()
}) })
w := PerformRequest(route, http.MethodGet, "/NotFound") w := PerformRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, 421, w.Code) assert.Equal(t, http.StatusMisdirectedRequest, w.Code)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }

43
tree.go
View File

@ -351,7 +351,10 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
} }
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0] pathSeg := ""
if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
}
panic("catch-all wildcard '" + path + panic("catch-all wildcard '" + path +
"' in new path '" + fullPath + "' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg + "' conflicts with existing path segment '" + pathSeg +
@ -478,7 +481,7 @@ walk: // Outer loop for walking the tree
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
value.tsr = path == "/" && n.handlers != nil value.tsr = path == "/" && n.handlers != nil
return return value
} }
// Handle wildcard child, which is always at the end of the array // Handle wildcard child, which is always at the end of the array
@ -497,7 +500,14 @@ walk: // Outer loop for walking the tree
} }
// Save param value // Save param value
if params != nil && cap(*params) > 0 { if params != nil {
// Preallocate capacity if necessary
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
*params = newParams
}
if value.params == nil { if value.params == nil {
value.params = params value.params = params
} }
@ -526,12 +536,12 @@ walk: // Outer loop for walking the tree
// ... but we can't // ... but we can't
value.tsr = len(path) == end+1 value.tsr = len(path) == end+1
return return value
} }
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath value.fullPath = n.fullPath
return return value
} }
if len(n.children) == 1 { if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
@ -539,11 +549,18 @@ walk: // Outer loop for walking the tree
n = n.children[0] n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
} }
return return value
case catchAll: case catchAll:
// Save param value // Save param value
if params != nil { if params != nil {
// Preallocate capacity if necessary
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
*params = newParams
}
if value.params == nil { if value.params == nil {
value.params = params value.params = params
} }
@ -564,7 +581,7 @@ walk: // Outer loop for walking the tree
value.handlers = n.handlers value.handlers = n.handlers
value.fullPath = n.fullPath value.fullPath = n.fullPath
return return value
default: default:
panic("invalid node type") panic("invalid node type")
@ -595,7 +612,7 @@ walk: // Outer loop for walking the tree
// Check if this node has a handle registered. // Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath value.fullPath = n.fullPath
return return value
} }
// If there is no handle for this route, but this route has a // If there is no handle for this route, but this route has a
@ -603,12 +620,12 @@ walk: // Outer loop for walking the tree
// additional trailing slash // additional trailing slash
if path == "/" && n.wildChild && n.nType != root { if path == "/" && n.wildChild && n.nType != root {
value.tsr = true value.tsr = true
return return value
} }
if path == "/" && n.nType == static { if path == "/" && n.nType == static {
value.tsr = true value.tsr = true
return return value
} }
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
@ -618,11 +635,11 @@ walk: // Outer loop for walking the tree
n = n.children[i] n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) || value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) (n.nType == catchAll && n.children[0].handlers != nil)
return return value
} }
} }
return return value
} }
// Nothing found. We can recommend to redirect to the same URL with an // Nothing found. We can recommend to redirect to the same URL with an
@ -648,7 +665,7 @@ walk: // Outer loop for walking the tree
} }
} }
return return value
} }
} }

View File

@ -417,6 +417,8 @@ func TestTreeWildcardConflict(t *testing.T) {
{"/user_:name", false}, {"/user_:name", false},
{"/id:id", false}, {"/id:id", false},
{"/id/:id", false}, {"/id/:id", false},
{"/static/*file", false},
{"/static/", true},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -893,9 +895,9 @@ func TestTreeInvalidNodeType(t *testing.T) {
func TestTreeInvalidParamsType(t *testing.T) { func TestTreeInvalidParamsType(t *testing.T) {
tree := &node{} tree := &node{}
tree.wildChild = true // add a child with wildcard
tree.children = append(tree.children, &node{}) route := "/:path"
tree.children[0].nType = 2 tree.addRoute(route, fakeHandler(route))
// set invalid Params type // set invalid Params type
params := make(Params, 0) params := make(Params, 0)
@ -904,6 +906,34 @@ func TestTreeInvalidParamsType(t *testing.T) {
tree.getValue("/test", &params, getSkippedNodes(), false) tree.getValue("/test", &params, getSkippedNodes(), false)
} }
func TestTreeExpandParamsCapacity(t *testing.T) {
data := []struct {
path string
}{
{"/:path"},
{"/*path"},
}
for _, item := range data {
tree := &node{}
tree.addRoute(item.path, fakeHandler(item.path))
params := make(Params, 0)
value := tree.getValue("/test", &params, getSkippedNodes(), false)
if value.params == nil {
t.Errorf("Expected %s params to be set, but they weren't", item.path)
continue
}
if len(*value.params) != 1 {
t.Errorf("Wrong number of %s params: got %d, want %d",
item.path, len(*value.params), 1)
continue
}
}
}
func TestTreeWildcardConflictEx(t *testing.T) { func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct { conflicts := [...]struct {
route string route string

View File

@ -5,4 +5,4 @@
package gin package gin
// Version is the current gin framework's version. // Version is the current gin framework's version.
const Version = "v1.9.0" const Version = "v1.10.0"