mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-14 20:22:20 +08:00
Merge branch 'master' into http3
This commit is contained in:
commit
52092b2cca
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -30,7 +30,7 @@ func main() {
|
||||
|
||||
<!-- Your expectation result of 'curl' command, like -->
|
||||
```
|
||||
$ curl http://localhost:8201/hello/world
|
||||
$ curl http://localhost:9000/hello/world
|
||||
Hello world
|
||||
```
|
||||
|
||||
@ -38,7 +38,7 @@ Hello world
|
||||
|
||||
<!-- Actual result showing the problem -->
|
||||
```
|
||||
$ curl -i http://localhost:8201/hello/world
|
||||
$ curl -i http://localhost:9000/hello/world
|
||||
<YOUR RESULT>
|
||||
```
|
||||
|
||||
|
14
.github/workflows/codeql.yml
vendored
14
.github/workflows/codeql.yml
vendored
@ -7,12 +7,12 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 17 * * 5'
|
||||
- cron: "0 17 * * 5"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@ -29,15 +29,15 @@ jobs:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
# TODO: Enable for javascript later
|
||||
language: [ 'go']
|
||||
language: ["go"]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
33
.github/workflows/gin.yml
vendored
33
.github/workflows/gin.yml
vendored
@ -15,24 +15,28 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
go-version: '^1.18'
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
- name: Setup golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3.4.0
|
||||
uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
version: v1.52.2
|
||||
version: v1.56.2
|
||||
args: --verbose
|
||||
test:
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
go: ['1.18', '1.19', '1.20']
|
||||
test-tags: ['', '-tags nomsgpack', '-tags "sonic avx"', '-tags go_json']
|
||||
go: ["1.18", "1.19", "1.20", "1.21", "1.22"]
|
||||
test-tags:
|
||||
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
go-build: ~/.cache/go-build
|
||||
@ -46,16 +50,17 @@ jobs:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ matrix.go-build }}
|
||||
@ -68,10 +73,10 @@ jobs:
|
||||
run: make test
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
|
||||
|
||||
- name: Format
|
||||
if: matrix.go-version == '1.20.x'
|
||||
if: matrix.go-version == '1.22.x'
|
||||
run: diff -u <(echo -n) <(gofmt -d .)
|
||||
|
21
.github/workflows/goreleaser.yml
vendored
21
.github/workflows/goreleaser.yml
vendored
@ -3,7 +3,7 @@ name: Goreleaser
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -12,23 +12,20 @@ jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.20
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
go-version: "^1"
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,3 +5,7 @@ count.out
|
||||
test
|
||||
profile.out
|
||||
tmp.out
|
||||
|
||||
# Develop tools
|
||||
.idea/
|
||||
.vscode/
|
||||
|
@ -3,7 +3,6 @@ run:
|
||||
linters:
|
||||
enable:
|
||||
- asciicheck
|
||||
- depguard
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errcheck
|
||||
|
@ -1,8 +1,7 @@
|
||||
project_name: gin
|
||||
|
||||
builds:
|
||||
-
|
||||
# If true, skip the build.
|
||||
- # If true, skip the build.
|
||||
# Useful for library projects.
|
||||
# Default is false
|
||||
skip: true
|
||||
@ -10,7 +9,7 @@ builds:
|
||||
changelog:
|
||||
# Set it to true if you wish to skip the changelog generation.
|
||||
# This may result in an empty release notes on GitHub/GitLab/Gitea.
|
||||
skip: false
|
||||
disable: false
|
||||
|
||||
# Changelog generation implementation to use.
|
||||
#
|
||||
@ -21,7 +20,7 @@ changelog:
|
||||
# - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
|
||||
#
|
||||
# Defaults to `git`.
|
||||
use: git
|
||||
use: github
|
||||
|
||||
# Sorts the changelog by the commit's messages.
|
||||
# Could either be asc, desc or empty
|
||||
@ -38,20 +37,20 @@ changelog:
|
||||
- title: Features
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
order: 0
|
||||
- title: 'Bug fixes'
|
||||
- title: "Bug fixes"
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: 'Enhancements'
|
||||
- title: "Enhancements"
|
||||
regexp: "^.*chore[(\\w)]*:+.*$"
|
||||
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
|
||||
order: 999
|
||||
|
||||
filters:
|
||||
# Commit messages matching the regexp listed here will be removed from
|
||||
# the changelog
|
||||
# Default is empty
|
||||
exclude:
|
||||
- '^docs'
|
||||
- 'CICD'
|
||||
- typo
|
||||
|
95
CHANGELOG.md
95
CHANGELOG.md
@ -1,5 +1,100 @@
|
||||
# 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
|
||||
|
||||
### BREAK CHANGES
|
||||
|
1
Makefile
1
Makefile
@ -42,6 +42,7 @@ fmt-check:
|
||||
exit 1; \
|
||||
fi;
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
$(GO) vet $(VETPACKAGES)
|
||||
|
||||
|
25
auth.go
25
auth.go
@ -16,6 +16,9 @@ import (
|
||||
// AuthUserKey is the cookie name for user credential in basic auth.
|
||||
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.
|
||||
type Accounts map[string]string
|
||||
|
||||
@ -89,3 +92,25 @@ func authorizationHeader(user, password string) string {
|
||||
base := user + ":" + password
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
37
auth_test.go
37
auth_test.go
@ -137,3 +137,40 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
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"))
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ const (
|
||||
MIMEMSGPACK = "application/x-msgpack"
|
||||
MIMEMSGPACK2 = "application/msgpack"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
)
|
||||
|
||||
@ -72,18 +73,18 @@ var Validator StructValidator = &defaultValidator{}
|
||||
// These implement the Binding interface and can be used to bind the data
|
||||
// present in the request to struct instances.
|
||||
var (
|
||||
JSON = jsonBinding{}
|
||||
XML = xmlBinding{}
|
||||
Form = formBinding{}
|
||||
Query = queryBinding{}
|
||||
FormPost = formPostBinding{}
|
||||
FormMultipart = formMultipartBinding{}
|
||||
ProtoBuf = protobufBinding{}
|
||||
MsgPack = msgpackBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
TOML = tomlBinding{}
|
||||
JSON BindingBody = jsonBinding{}
|
||||
XML BindingBody = xmlBinding{}
|
||||
Form Binding = formBinding{}
|
||||
Query Binding = queryBinding{}
|
||||
FormPost Binding = formPostBinding{}
|
||||
FormMultipart Binding = formMultipartBinding{}
|
||||
ProtoBuf BindingBody = protobufBinding{}
|
||||
MsgPack BindingBody = msgpackBinding{}
|
||||
YAML BindingBody = yamlBinding{}
|
||||
Uri BindingUri = uriBinding{}
|
||||
Header Binding = headerBinding{}
|
||||
TOML BindingBody = tomlBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -102,7 +103,7 @@ func Default(method, contentType string) Binding {
|
||||
return ProtoBuf
|
||||
case MIMEMSGPACK, MIMEMSGPACK2:
|
||||
return MsgPack
|
||||
case MIMEYAML:
|
||||
case MIMEYAML, MIMEYAML2:
|
||||
return YAML
|
||||
case MIMETOML:
|
||||
return TOML
|
||||
|
@ -19,6 +19,7 @@ const (
|
||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
)
|
||||
|
||||
@ -96,7 +97,7 @@ func Default(method, contentType string) Binding {
|
||||
return XML
|
||||
case MIMEPROTOBUF:
|
||||
return ProtoBuf
|
||||
case MIMEYAML:
|
||||
case MIMEYAML, MIMEYAML2:
|
||||
return YAML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
|
@ -164,6 +164,8 @@ func TestBindingDefault(t *testing.T) {
|
||||
|
||||
assert.Equal(t, YAML, Default("POST", 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("PUT", MIMETOML))
|
||||
|
@ -54,7 +54,10 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
||||
value := reflect.ValueOf(obj)
|
||||
switch value.Kind() {
|
||||
case reflect.Ptr:
|
||||
if value.Elem().Kind() != reflect.Struct {
|
||||
return v.ValidateStruct(value.Elem().Interface())
|
||||
}
|
||||
return v.validateStruct(obj)
|
||||
case reflect.Struct:
|
||||
return v.validateStruct(obj)
|
||||
case reflect.Slice, reflect.Array:
|
||||
|
@ -7,6 +7,7 @@ package binding
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -164,6 +165,23 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
||||
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) {
|
||||
vs, ok := form[tagValue]
|
||||
if !ok && !opt.isDefaultExists {
|
||||
@ -193,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
if len(vs) > 0 {
|
||||
val = vs[0]
|
||||
}
|
||||
if ok, err := trySetCustom(val, value); ok {
|
||||
return ok, err
|
||||
}
|
||||
return true, setWithProperType(val, value, field)
|
||||
}
|
||||
}
|
||||
@ -235,10 +256,17 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
||||
switch value.Interface().(type) {
|
||||
case time.Time:
|
||||
return setTimeField(val, field, value)
|
||||
case multipart.FileHeader:
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Map:
|
||||
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:
|
||||
return errUnknownType
|
||||
}
|
||||
@ -369,11 +397,8 @@ func setTimeDuration(val string, value reflect.Value) error {
|
||||
}
|
||||
|
||||
func head(str, sep string) (head string, tail string) {
|
||||
idx := strings.Index(str, sep)
|
||||
if idx < 0 {
|
||||
return str, ""
|
||||
}
|
||||
return str[:idx], str[idx+len(sep):]
|
||||
head, tail, _ = strings.Cut(str, sep)
|
||||
return head, tail
|
||||
}
|
||||
|
||||
func setFormMap(ptr any, form map[string][]string) error {
|
||||
|
@ -5,7 +5,11 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -43,6 +47,7 @@ func TestMappingBaseTypes(t *testing.T) {
|
||||
{"zero value", struct{ F uint }{}, "", uint(0)},
|
||||
{"zero value", struct{ F bool }{}, "", false},
|
||||
{"zero value", struct{ F float32 }{}, "", float32(0)},
|
||||
{"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}},
|
||||
} {
|
||||
tp := reflect.TypeOf(tt.value)
|
||||
testName := tt.name + ":" + tp.Field(0).Type.String()
|
||||
@ -269,6 +274,39 @@ func TestMappingStructField(t *testing.T) {
|
||||
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) {
|
||||
var s struct {
|
||||
M map[string]int
|
||||
@ -288,3 +326,99 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
|
||||
err := mappingByPtr(&s, formSource{}, "form")
|
||||
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)
|
||||
}
|
||||
|
@ -192,6 +192,30 @@ func TestValidatePrimitives(t *testing.T) {
|
||||
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
|
||||
// custom validation can be registered on it.
|
||||
// The `notone` binding directive is for custom validation and registered later.
|
||||
|
13
codecov.yml
Normal file
13
codecov.yml
Normal file
@ -0,0 +1,13 @@
|
||||
coverage:
|
||||
require_ci_to_pass: true
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 99%
|
||||
threshold: 99%
|
||||
|
||||
patch:
|
||||
default:
|
||||
target: 99%
|
||||
threshold: 95%
|
75
context.go
75
context.go
@ -43,6 +43,10 @@ const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
|
||||
// ContextKey is the key that a Context returns itself for.
|
||||
const ContextKey = "_gin-gonic/gin/contextkey"
|
||||
|
||||
type ContextKeyType int
|
||||
|
||||
const ContextRequestKey ContextKeyType = 0
|
||||
|
||||
// abortIndex represents a typical value used in abort functions.
|
||||
const abortIndex int8 = math.MaxInt8 >> 1
|
||||
|
||||
@ -113,20 +117,27 @@ func (c *Context) Copy() *Context {
|
||||
cp := Context{
|
||||
writermem: c.writermem,
|
||||
Request: c.Request,
|
||||
Params: c.Params,
|
||||
engine: c.engine,
|
||||
}
|
||||
|
||||
cp.writermem.ResponseWriter = nil
|
||||
cp.Writer = &cp.writermem
|
||||
cp.index = abortIndex
|
||||
cp.handlers = nil
|
||||
cp.Keys = map[string]any{}
|
||||
for k, v := range c.Keys {
|
||||
cp.fullPath = c.fullPath
|
||||
|
||||
cKeys := c.Keys
|
||||
cp.Keys = make(map[string]any, len(cKeys))
|
||||
c.mu.RLock()
|
||||
for k, v := range cKeys {
|
||||
cp.Keys[k] = v
|
||||
}
|
||||
paramCopy := make([]Param, len(cp.Params))
|
||||
copy(paramCopy, cp.Params)
|
||||
cp.Params = paramCopy
|
||||
c.mu.RUnlock()
|
||||
|
||||
cParams := c.Params
|
||||
cp.Params = make([]Param, len(cParams))
|
||||
copy(cp.Params, cParams)
|
||||
|
||||
return &cp
|
||||
}
|
||||
|
||||
@ -386,7 +397,7 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
|
||||
//
|
||||
// router.GET("/user/:id", func(c *gin.Context) {
|
||||
// // a GET request to /user/john
|
||||
// id := c.Param("id") // id == "/john"
|
||||
// id := c.Param("id") // id == "john"
|
||||
// // a GET request to /user/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.
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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]).
|
||||
@ -873,6 +904,9 @@ func (c *Context) GetHeader(key string) string {
|
||||
|
||||
// GetRawData returns stream data.
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1052,11 +1086,17 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
|
||||
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
|
||||
// On the client side, the file will typically be downloaded with the given filename
|
||||
func (c *Context) FileAttachment(filepath, filename string) {
|
||||
if isASCII(filename) {
|
||||
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+escapeQuotes(filename)+`"`)
|
||||
} else {
|
||||
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 *****/
|
||||
/************************************/
|
||||
|
||||
// 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.
|
||||
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 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.
|
||||
func (c *Context) Done() <-chan struct{} {
|
||||
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
|
||||
if !c.hasRequestContext() {
|
||||
return nil
|
||||
}
|
||||
return c.Request.Context().Done()
|
||||
@ -1192,7 +1239,7 @@ func (c *Context) Done() <-chan struct{} {
|
||||
|
||||
// Err returns nil when c.Request has no Context.
|
||||
func (c *Context) Err() error {
|
||||
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
|
||||
if !c.hasRequestContext() {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
// the same key returns the same result.
|
||||
func (c *Context) Value(key any) any {
|
||||
if key == 0 {
|
||||
if key == ContextRequestKey {
|
||||
return c.Request
|
||||
}
|
||||
if key == ContextKey {
|
||||
@ -1213,7 +1260,7 @@ func (c *Context) Value(key any) any {
|
||||
return val
|
||||
}
|
||||
}
|
||||
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
|
||||
if !c.hasRequestContext() {
|
||||
return nil
|
||||
}
|
||||
return c.Request.Context().Value(key)
|
||||
|
314
context_test.go
314
context_test.go
@ -324,6 +324,7 @@ func TestContextCopy(t *testing.T) {
|
||||
c.handlers = HandlersChain{func(c *Context) {}}
|
||||
c.Params = Params{Param{Key: "foo", Value: "bar"}}
|
||||
c.Set("foo", "bar")
|
||||
c.fullPath = "/hola"
|
||||
|
||||
cp := c.Copy()
|
||||
assert.Nil(t, cp.handlers)
|
||||
@ -336,6 +337,7 @@ func TestContextCopy(t *testing.T) {
|
||||
assert.Equal(t, cp.Params, c.Params)
|
||||
cp.Set("foo", "notBar")
|
||||
assert.False(t, cp.Keys["foo"] == c.Keys["foo"])
|
||||
assert.Equal(t, cp.fullPath, c.fullPath)
|
||||
}
|
||||
|
||||
func TestContextHandlerName(t *testing.T) {
|
||||
@ -998,7 +1000,7 @@ func TestContextRenderFile(t *testing.T) {
|
||||
c.File("./gin.go")
|
||||
|
||||
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,
|
||||
// else, Content-Type='text/x-go; charset=utf-8'
|
||||
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
|
||||
@ -1012,7 +1014,7 @@ func TestContextRenderFileFromFS(t *testing.T) {
|
||||
c.FileFromFS("./gin.go", Dir(".", false))
|
||||
|
||||
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,
|
||||
// else, Content-Type='text/x-go; charset=utf-8'
|
||||
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
|
||||
@ -1028,10 +1030,24 @@ func TestContextRenderAttachment(t *testing.T) {
|
||||
c.FileAttachment("./gin.go", newFilename)
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
@ -1041,12 +1057,12 @@ func TestContextRenderUTF8Attachment(t *testing.T) {
|
||||
c.FileAttachment("./gin.go", newFilename)
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
@ -1055,7 +1071,7 @@ func TestContextRenderYAML(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
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
|
||||
@ -1203,7 +1219,7 @@ func TestContextNegotiationWithYAML(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
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) {
|
||||
@ -1555,6 +1571,12 @@ func TestContextClientIP(t *testing.T) {
|
||||
c.Request.Header.Del("CF-Connecting-IP")
|
||||
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 = ""
|
||||
|
||||
// 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-Appengine-Remote-Addr", "50.50.50.50")
|
||||
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.engine.TrustedPlatform = ""
|
||||
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) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
@ -1962,7 +2242,7 @@ func TestContextGolangContext(t *testing.T) {
|
||||
ti, ok := c.Deadline()
|
||||
assert.Equal(t, ti, time.Time{})
|
||||
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.Nil(t, c.Value("foo"))
|
||||
|
||||
@ -2162,6 +2442,24 @@ func TestRemoteIPFail(t *testing.T) {
|
||||
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) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
// enable ContextWithFallback feature flag
|
||||
|
14
debug.go
14
debug.go
@ -23,6 +23,9 @@ func IsDebugging() bool {
|
||||
// DebugPrintRouteFunc indicates debug log output format.
|
||||
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) {
|
||||
if IsDebugging() {
|
||||
nuHandlers := len(handlers)
|
||||
@ -48,12 +51,19 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||
}
|
||||
|
||||
func debugPrint(format string, values ...any) {
|
||||
if IsDebugging() {
|
||||
if !IsDebugging() {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -12,6 +12,8 @@ import (
|
||||
|
||||
// BindWith binds the passed struct pointer using the specified binding engine.
|
||||
// See the binding package.
|
||||
//
|
||||
// Deprecated: Use MustBindWith or ShouldBindWith.
|
||||
func (c *Context) BindWith(obj any, b binding.Binding) error {
|
||||
log.Println(`BindWith(\"any, binding.Binding\") error is going to
|
||||
be deprecated, please check issue #662 and either use MustBindWith() if you
|
||||
|
100
docs/doc.md
100
docs/doc.md
@ -27,6 +27,7 @@
|
||||
- [Only Bind Query String](#only-bind-query-string)
|
||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||
- [Bind Uri](#bind-uri)
|
||||
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
|
||||
- [Bind Header](#bind-header)
|
||||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||
- [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" "
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```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
|
||||
func SomeHandler(c *gin.Context) {
|
||||
@ -1931,7 +2015,7 @@ func SomeHandler(c *gin.Context) {
|
||||
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
|
||||
c.String(http.StatusOK, `the body should be formB JSON`)
|
||||
// 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`)
|
||||
} else {
|
||||
...
|
||||
@ -2176,10 +2260,16 @@ import (
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
// Use predefined header gin.PlatformXXX
|
||||
// Google App Engine
|
||||
router.TrustedPlatform = gin.PlatformGoogleAppEngine
|
||||
// Or set your own trusted request header for another trusted proxy service
|
||||
// Don't set it to any suspect request header, it's unsafe
|
||||
router.TrustedPlatform = "X-CDN-IP"
|
||||
// Cloudflare
|
||||
router.TrustedPlatform = gin.PlatformCloudflare
|
||||
// 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) {
|
||||
// If you set TrustedPlatform, ClientIP() will resolve the
|
||||
|
62
gin.go
62
gin.go
@ -47,6 +47,9 @@ var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
// OptionFunc defines the function to change the default configuration
|
||||
type OptionFunc func(*Engine)
|
||||
|
||||
// HandlersChain defines a HandlerFunc slice.
|
||||
type HandlersChain []HandlerFunc
|
||||
|
||||
@ -77,6 +80,8 @@ const (
|
||||
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
|
||||
// the client's 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.
|
||||
@ -180,7 +185,7 @@ var _ IRouter = (*Engine)(nil)
|
||||
// - ForwardedByClientIP: true
|
||||
// - UseRawPath: false
|
||||
// - UnescapePathValues: true
|
||||
func New() *Engine {
|
||||
func New(opts ...OptionFunc) *Engine {
|
||||
debugPrintWARNINGNew()
|
||||
engine := &Engine{
|
||||
RouterGroup: RouterGroup{
|
||||
@ -209,15 +214,15 @@ func New() *Engine {
|
||||
engine.pool.New = func() any {
|
||||
return engine.allocateContext(engine.maxParams)
|
||||
}
|
||||
return engine
|
||||
return engine.With(opts...)
|
||||
}
|
||||
|
||||
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
|
||||
func Default() *Engine {
|
||||
func Default(opts ...OptionFunc) *Engine {
|
||||
debugPrintWARNINGDefault()
|
||||
engine := New()
|
||||
engine.Use(Logger(), Recovery())
|
||||
return engine
|
||||
return engine.With(opts...)
|
||||
}
|
||||
|
||||
func (engine *Engine) Handler() http.Handler {
|
||||
@ -311,6 +316,15 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
||||
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() {
|
||||
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
|
||||
}
|
||||
@ -334,7 +348,6 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
||||
}
|
||||
root.addRoute(path, handlers)
|
||||
|
||||
// Update maxParams
|
||||
if paramsCount := countParams(path); paramsCount > engine.maxParams {
|
||||
engine.maxParams = paramsCount
|
||||
}
|
||||
@ -370,6 +383,24 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
|
||||
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) {
|
||||
if engine.trustedProxies == nil {
|
||||
return nil, nil
|
||||
@ -473,7 +504,14 @@ func parseIP(ip string) net.IP {
|
||||
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)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
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() {
|
||||
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())
|
||||
@ -634,17 +672,25 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
}
|
||||
|
||||
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 {
|
||||
if tree.method == httpMethod {
|
||||
continue
|
||||
}
|
||||
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
|
||||
allowed = append(allowed, tree.method)
|
||||
}
|
||||
}
|
||||
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
|
||||
serveError(c, http.StatusNotFound, default404Body)
|
||||
}
|
||||
|
58
gin_test.go
58
gin_test.go
@ -14,6 +14,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@ -696,3 +697,60 @@ func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo)
|
||||
|
||||
func handlerTest1(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
36
go.mod
@ -3,24 +3,26 @@ module github.com/gin-gonic/gin
|
||||
go 1.20
|
||||
|
||||
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/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/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.18
|
||||
github.com/pelletier/go-toml/v2 v2.0.7
|
||||
github.com/quic-go/quic-go v0.34.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/ugorji/go/codec v1.2.11
|
||||
golang.org/x/net v0.9.0
|
||||
google.golang.org/protobuf v1.30.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ugorji/go/codec v1.2.12
|
||||
golang.org/x/net v0.25.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
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/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
@ -30,6 +32,9 @@ require (
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.2 // 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/onsi/ginkgo/v2 v2.2.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-20 v0.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||
golang.org/x/mod v0.8.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
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
)
|
||||
|
9
go.sum
9
go.sum
@ -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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
17
logger.go
17
logger.go
@ -47,8 +47,15 @@ type LoggerConfig struct {
|
||||
// SkipPaths is an url path array which logs are not written.
|
||||
// Optional.
|
||||
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
|
||||
type LogFormatter func(params LogFormatterParams) string
|
||||
|
||||
@ -83,6 +90,8 @@ func (p *LogFormatterParams) StatusCodeColor() string {
|
||||
code := p.StatusCode
|
||||
|
||||
switch {
|
||||
case code >= http.StatusContinue && code < http.StatusOK:
|
||||
return white
|
||||
case code >= http.StatusOK && code < http.StatusMultipleChoices:
|
||||
return green
|
||||
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
|
||||
@ -239,8 +248,11 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Log only when path is not being skipped
|
||||
if _, ok := skip[path]; !ok {
|
||||
// Log only when it is not being skipped
|
||||
if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) {
|
||||
return
|
||||
}
|
||||
|
||||
param := LogFormatterParams{
|
||||
Request: c.Request,
|
||||
isTerm: isTerm,
|
||||
@ -266,5 +278,4 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
||||
|
||||
fmt.Fprint(out, formatter(param))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -310,6 +310,7 @@ func TestColorForStatus(t *testing.T) {
|
||||
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, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||
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(), "")
|
||||
}
|
||||
|
||||
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) {
|
||||
New()
|
||||
assert.Equal(t, autoColor, consoleColorMode)
|
||||
|
@ -6,6 +6,7 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -80,6 +81,10 @@ func TestPathCleanMallocs(t *testing.T) {
|
||||
t.Skip("skipping malloc count in short mode")
|
||||
}
|
||||
|
||||
if runtime.GOMAXPROCS(0) > 1 {
|
||||
t.Skip("skipping malloc count; GOMAXPROCS>1")
|
||||
}
|
||||
|
||||
for _, test := range cleanTests {
|
||||
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
|
||||
assert.EqualValues(t, allocs, 0)
|
||||
|
@ -15,22 +15,22 @@ type Render interface {
|
||||
}
|
||||
|
||||
var (
|
||||
_ Render = JSON{}
|
||||
_ Render = IndentedJSON{}
|
||||
_ Render = SecureJSON{}
|
||||
_ Render = JsonpJSON{}
|
||||
_ Render = XML{}
|
||||
_ Render = String{}
|
||||
_ Render = Redirect{}
|
||||
_ Render = Data{}
|
||||
_ Render = HTML{}
|
||||
_ HTMLRender = HTMLDebug{}
|
||||
_ HTMLRender = HTMLProduction{}
|
||||
_ Render = YAML{}
|
||||
_ Render = Reader{}
|
||||
_ Render = AsciiJSON{}
|
||||
_ Render = ProtoBuf{}
|
||||
_ Render = TOML{}
|
||||
_ Render = (*JSON)(nil)
|
||||
_ Render = (*IndentedJSON)(nil)
|
||||
_ Render = (*SecureJSON)(nil)
|
||||
_ Render = (*JsonpJSON)(nil)
|
||||
_ Render = (*XML)(nil)
|
||||
_ Render = (*String)(nil)
|
||||
_ Render = (*Redirect)(nil)
|
||||
_ Render = (*Data)(nil)
|
||||
_ Render = (*HTML)(nil)
|
||||
_ HTMLRender = (*HTMLDebug)(nil)
|
||||
_ HTMLRender = (*HTMLProduction)(nil)
|
||||
_ Render = (*YAML)(nil)
|
||||
_ Render = (*Reader)(nil)
|
||||
_ Render = (*AsciiJSON)(nil)
|
||||
_ Render = (*ProtoBuf)(nil)
|
||||
_ Render = (*TOML)(nil)
|
||||
)
|
||||
|
||||
func writeContentType(w http.ResponseWriter, value []string) {
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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"))
|
||||
}
|
||||
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{
|
||||
@ -234,12 +280,12 @@ b:
|
||||
d: [3, 4]
|
||||
`
|
||||
(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)
|
||||
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, "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{}
|
||||
@ -532,3 +578,16 @@ func TestRenderReaderNoContentLength(t *testing.T) {
|
||||
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||
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())
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ type YAML struct {
|
||||
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.
|
||||
func (r YAML) Render(w http.ResponseWriter) error {
|
||||
|
@ -156,3 +156,33 @@ func TestResponseWriterStatusCode(t *testing.T) {
|
||||
// status must be 200 although we tried to change it
|
||||
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")
|
||||
}
|
||||
|
@ -180,58 +180,58 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
|
||||
|
||||
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, 301, w.Code)
|
||||
assert.Equal(t, http.StatusMovedPermanently, w.Code)
|
||||
|
||||
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#?"})
|
||||
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"})
|
||||
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"})
|
||||
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"})
|
||||
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/../../"})
|
||||
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/../../../"})
|
||||
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"})
|
||||
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"})
|
||||
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/#"})
|
||||
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"})
|
||||
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"})
|
||||
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/"})
|
||||
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
|
||||
|
||||
@ -337,6 +337,45 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
|
||||
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
|
||||
func TestRouteStaticFile(t *testing.T) {
|
||||
// SETUP file
|
||||
@ -475,6 +514,18 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
|
||||
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) {
|
||||
router := New()
|
||||
router.HandleMethodNotAllowed = false
|
||||
@ -568,11 +619,11 @@ func TestRouterNotFound(t *testing.T) {
|
||||
router = New()
|
||||
router.NoRoute(func(c *Context) {
|
||||
if c.Request.RequestURI == "/login" {
|
||||
c.String(200, "login")
|
||||
c.String(http.StatusOK, "login")
|
||||
}
|
||||
})
|
||||
router.GET("/logout", func(c *Context) {
|
||||
c.String(200, "logout")
|
||||
c.String(http.StatusOK, "logout")
|
||||
})
|
||||
w = PerformRequest(router, http.MethodGet, "/login")
|
||||
assert.Equal(t, "login", w.Body.String())
|
||||
@ -584,7 +635,7 @@ func TestRouterStaticFSNotFound(t *testing.T) {
|
||||
router := New()
|
||||
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
|
||||
router.NoRoute(func(c *Context) {
|
||||
c.String(404, "non existent")
|
||||
c.String(http.StatusNotFound, "non existent")
|
||||
})
|
||||
|
||||
w := PerformRequest(router, http.MethodGet, "/nonexistent")
|
||||
@ -667,12 +718,12 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
|
||||
func TestRouteServeErrorWithWriteHeader(t *testing.T) {
|
||||
route := New()
|
||||
route.Use(func(c *Context) {
|
||||
c.Status(421)
|
||||
c.Status(http.StatusMisdirectedRequest)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
|
43
tree.go
43
tree.go
@ -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] == '/' {
|
||||
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 +
|
||||
"' in new path '" + fullPath +
|
||||
"' 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
|
||||
// trailing slash if a leaf exists for that path.
|
||||
value.tsr = path == "/" && n.handlers != nil
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// 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
|
||||
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 {
|
||||
value.params = params
|
||||
}
|
||||
@ -526,12 +536,12 @@ walk: // Outer loop for walking the tree
|
||||
|
||||
// ... but we can't
|
||||
value.tsr = len(path) == end+1
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
return value
|
||||
}
|
||||
if len(n.children) == 1 {
|
||||
// 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]
|
||||
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
|
||||
}
|
||||
return
|
||||
return value
|
||||
|
||||
case catchAll:
|
||||
// Save param value
|
||||
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 {
|
||||
value.params = params
|
||||
}
|
||||
@ -564,7 +581,7 @@ walk: // Outer loop for walking the tree
|
||||
|
||||
value.handlers = n.handlers
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
return value
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
@ -595,7 +612,7 @@ walk: // Outer loop for walking the tree
|
||||
// Check if this node has a handle registered.
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// 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
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
value.tsr = true
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
if path == "/" && n.nType == static {
|
||||
value.tsr = true
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// 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]
|
||||
value.tsr = (len(n.path) == 1 && n.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
|
||||
@ -648,7 +665,7 @@ walk: // Outer loop for walking the tree
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
|
36
tree_test.go
36
tree_test.go
@ -417,6 +417,8 @@ func TestTreeWildcardConflict(t *testing.T) {
|
||||
{"/user_:name", false},
|
||||
{"/id:id", false},
|
||||
{"/id/:id", false},
|
||||
{"/static/*file", false},
|
||||
{"/static/", true},
|
||||
}
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
@ -893,9 +895,9 @@ func TestTreeInvalidNodeType(t *testing.T) {
|
||||
|
||||
func TestTreeInvalidParamsType(t *testing.T) {
|
||||
tree := &node{}
|
||||
tree.wildChild = true
|
||||
tree.children = append(tree.children, &node{})
|
||||
tree.children[0].nType = 2
|
||||
// add a child with wildcard
|
||||
route := "/:path"
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
|
||||
// set invalid Params type
|
||||
params := make(Params, 0)
|
||||
@ -904,6 +906,34 @@ func TestTreeInvalidParamsType(t *testing.T) {
|
||||
tree.getValue("/test", ¶ms, 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", ¶ms, 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) {
|
||||
conflicts := [...]struct {
|
||||
route string
|
||||
|
@ -5,4 +5,4 @@
|
||||
package gin
|
||||
|
||||
// Version is the current gin framework's version.
|
||||
const Version = "v1.9.0"
|
||||
const Version = "v1.10.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user