diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index e86bc98f..96e70bba 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,7 +1,7 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- - It should pass all tests in the available continuous integration systems such as TravisCI.
+ - It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..632e8eb2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ - package-ecosystem: gomod
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml
new file mode 100644
index 00000000..15c2530a
--- /dev/null
+++ b/.github/workflows/gin.yml
@@ -0,0 +1,84 @@
+name: Run Tests
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Setup go
+ uses: actions/setup-go@v2
+ with:
+ go-version: '^1.16'
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ - name: Setup golangci-lint
+ uses: golangci/golangci-lint-action@v2
+ with:
+ version: v1.42.1
+ args: --verbose
+ test:
+ needs: lint
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ go: [1.13, 1.14, 1.15, 1.16, 1.17]
+ test-tags: ['', nomsgpack]
+ include:
+ - os: ubuntu-latest
+ go-build: ~/.cache/go-build
+ - os: macos-latest
+ go-build: ~/Library/Caches/go-build
+ name: ${{ matrix.os }} @ Go ${{ matrix.go }} ${{ matrix.test-tags }}
+ runs-on: ${{ matrix.os }}
+ env:
+ GO111MODULE: on
+ TESTTAGS: ${{ matrix.test-tags }}
+ GOPROXY: https://proxy.golang.org
+ steps:
+ - name: Set up Go ${{ matrix.go }}
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go }}
+
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.ref }}
+
+ - uses: actions/cache@v2
+ with:
+ path: |
+ ${{ matrix.go-build }}
+ ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Run Tests
+ run: make test
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v2
+ with:
+ flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
+ notification-gitter:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notification failure message
+ if: failure()
+ run: |
+ PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
+ curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" -d level=error https://webhooks.gitter.im/e/7f95bf605c4d356372f4
+ - name: Notification success message
+ if: success()
+ run: |
+ PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
+ curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" https://webhooks.gitter.im/e/7f95bf605c4d356372f4
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 00000000..c5e1de38
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,39 @@
+run:
+ timeout: 5m
+linters:
+ enable:
+ - asciicheck
+ - depguard
+ - dogsled
+ - durationcheck
+ - errcheck
+ - errorlint
+ - exportloopref
+ - gci
+ - gofmt
+ - goimports
+ - gosec
+ - misspell
+ - nakedret
+ - nilerr
+ - nolintlint
+ - revive
+ - wastedassign
+issues:
+ exclude-rules:
+ - linters:
+ - structcheck
+ - unused
+ text: "`data` is unused"
+ - linters:
+ - staticcheck
+ text: "SA1019:"
+ - linters:
+ - revive
+ text: "var-naming:"
+ - linters:
+ - revive
+ text: "exported:"
+ - path: _test\.go
+ linters:
+ - gosec # security is not make sense in tests
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d7086b38..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-language: go
-
-matrix:
- fast_finish: true
- include:
- - go: 1.11.x
- env: GO111MODULE=on
- - go: 1.12.x
- env: GO111MODULE=on
- - go: 1.13.x
- - go: 1.13.x
- env:
- - TESTTAGS=nomsgpack
- - go: 1.14.x
- - go: 1.14.x
- env:
- - TESTTAGS=nomsgpack
- - go: 1.15.x
- - go: 1.15.x
- env:
- - TESTTAGS=nomsgpack
- - go: master
-
-git:
- depth: 10
-
-before_install:
- - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
-
-install:
- - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; fi
- - if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- - if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
-
-go_import_path: github.com/gin-gonic/gin
-
-script:
- - make vet
- - make fmt-check
- - make misspell-check
- - make test
-
-after_success:
- - bash <(curl -s https://codecov.io/bash)
-
-notifications:
- webhooks:
- urls:
- - https://webhooks.gitter.im/e/7f95bf605c4d356372f4
- on_success: change # options: [always|never|change] default: always
- on_failure: always # options: [always|never|change] default: always
- on_start: false # default: false
diff --git a/AUTHORS.md b/AUTHORS.md
index dda19bcf..c634e6be 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -156,7 +156,7 @@ People and companies, who have contributed, in alphabetical order.
- Fix variadic parameter in the flexible render API
- Fix Corrupted plain render
- Add Pluggable View Renderer Example
-
+
**@msemenistyi (Mykyta Semenistyi)**
- update Readme.md. Add code to String method
@@ -190,6 +190,8 @@ People and companies, who have contributed, in alphabetical order.
**@rogierlommers (Rogier Lommers)**
- Add updated static serve example
+**@rw-access (Ross Wolf)**
+- Added support to mix exact and param routes
**@se77en (Damon Zhao)**
- Improve color logging
diff --git a/BENCHMARKS.md b/BENCHMARKS.md
index 0f59b509..c11ee99a 100644
--- a/BENCHMARKS.md
+++ b/BENCHMARKS.md
@@ -1,11 +1,11 @@
# Benchmark System
-**VM HOST:** Travis
-**Machine:** Ubuntu 16.04.6 LTS x64
-**Date:** May 04th, 2020
+**VM HOST:** Travis
+**Machine:** Ubuntu 16.04.6 LTS x64
+**Date:** May 04th, 2020
**Version:** Gin v1.6.3
-**Go Version:** 1.14.2 linux/amd64
+**Go Version:** 1.14.2 linux/amd64
**Source:** [Go HTTP Router Benchmark](https://github.com/gin-gonic/go-http-routing-benchmark)
**Result:** [See the gist](https://gist.github.com/appleboy/b5f2ecfaf50824ae9c64dcfb9165ae5e) or [Travis result](https://travis-ci.org/github/gin-gonic/go-http-routing-benchmark/jobs/682947061)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ac51ad3..308af74c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,56 @@
# Gin ChangeLog
+## Gin v1.7.3
+
+### BUGFIXES
+
+* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
+
+## Gin v1.7.2
+
+### BUGFIXES
+
+* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
+
+## Gin v1.7.1
+
+### BUGFIXES
+
+* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
+
+## Gin v1.7.0
+
+### BUGFIXES
+
+* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
+* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
+* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
+
+### ENHANCEMENTS
+
+* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
+* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
+* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
+* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
+* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
+* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
+* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
+* remove a unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
+* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
+* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
+* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
+* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
+* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
+* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
+* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
+* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
+* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
+* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
+* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
+* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
+* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
+* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
+
## Gin v1.6.3
### ENHANCEMENTS
@@ -215,12 +266,12 @@
## Gin 1.1
-- [NEW] Implement QueryArray and PostArray methods
-- [NEW] Refactor GetQuery and GetPostForm
-- [NEW] Add contribution guide
+- [NEW] Implement QueryArray and PostArray methods
+- [NEW] Refactor GetQuery and GetPostForm
+- [NEW] Add contribution guide
- [FIX] Corrected typos in README
-- [FIX] Removed additional Iota
-- [FIX] Changed imports to gopkg instead of github in README (#733)
+- [FIX] Removed additional Iota
+- [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty
## Gin 1.0rc2 (...)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 98d758ef..d1c723c6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-## Contributing
+## Contributing
- With issues:
- Use the search tool before opening a new issue.
@@ -8,6 +8,6 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- - It should pass all tests in the available continuous integration systems such as TravisCI.
+ - It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
diff --git a/Makefile b/Makefile
index 1a991939..5d55b444 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,6 @@
GO ?= go
GOFMT ?= gofmt "-s"
+GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go")
@@ -67,5 +68,10 @@ misspell:
.PHONY: tools
tools:
- go install golang.org/x/lint/golint; \
- go install github.com/client9/misspell/cmd/misspell;
+ @if [ $(GO_VERSION) -gt 15 ]; then \
+ $(GO) install golang.org/x/lint/golint@latest; \
+ $(GO) install github.com/client9/misspell/cmd/misspell@latest; \
+ elif [ $(GO_VERSION) -lt 16 ]; then \
+ $(GO) install golang.org/x/lint/golint; \
+ $(GO) install github.com/client9/misspell/cmd/misspell; \
+ fi
diff --git a/README.md b/README.md
index fc1d01cf..cad746d6 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
+[![Build Status](https://github.com/gin-gonic/gin/workflows/Run%20Tests/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
@@ -23,7 +23,8 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Quick start](#quick-start)
- [Benchmarks](#benchmarks)
- [Gin v1. stable](#gin-v1-stable)
- - [Build with jsoniter](#build-with-jsoniter)
+ - [Build with jsoniter/go-json](#build-with-json-replacement)
+ - [Build without `MsgPack` rendering feature](#build-without-msgpack-rendering-feature)
- [API Examples](#api-examples)
- [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path)
@@ -77,6 +78,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie)
+ - [Don't trust all proxies](#don't-trust-all-proxies)
- [Testing](#testing)
- [Users](#users)
@@ -84,7 +86,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
To install Gin package, you need to install Go and set your Go workspace first.
-1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin.
+1. The first need [Go](https://golang.org/) installed (**version 1.13+ is required**), then you can use the below Go command to install Gin.
```sh
$ go get -u github.com/gin-gonic/gin
@@ -103,7 +105,7 @@ import "net/http"
```
## Quick start
-
+
```sh
# assume the following codes in example.go file
$ cat example.go
@@ -178,17 +180,32 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- [x] Zero allocation router.
- [x] Still the fastest http router and framework. From routing to writing.
-- [x] Complete suite of unit tests
-- [x] Battle tested
+- [x] Complete suite of unit tests.
+- [x] Battle tested.
- [x] API frozen, new releases will not break your code.
-## Build with [jsoniter](https://github.com/json-iterator/go)
+## Build with json replacement
-Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
+Gin uses `encoding/json` as default json package but you can change it by build from other tags.
+[jsoniter](https://github.com/json-iterator/go)
```sh
$ go build -tags=jsoniter .
```
+[go-json](https://github.com/goccy/go-json)
+```sh
+$ go build -tags=go_json .
+```
+
+## Build without `MsgPack` rendering feature
+
+Gin enables `MsgPack` rendering feature by default. But you can disable this feature by specifying `nomsgpack` build tag.
+
+```sh
+$ go build -tags=nomsgpack .
+```
+
+This is useful to reduce the binary size of executable files. See the [detail information](https://github.com/gin-gonic/gin/pull/1852).
## API Examples
@@ -240,7 +257,15 @@ func main() {
// For each matched request Context will hold the route definition
router.POST("/user/:name/*action", func(c *gin.Context) {
- c.FullPath() == "/user/:name/*action" // true
+ b := c.FullPath() == "/user/:name/*action" // true
+ c.String(http.StatusOK, "%t", b)
+ })
+
+ // This handler will add a new router for /user/groups.
+ // Exact routes are resolved before param routes, regardless of the order they were defined.
+ // Routes starting with /user/groups are never interpreted as /user/:name/... routes
+ router.GET("/user/groups", func(c *gin.Context) {
+ c.String(http.StatusOK, "The available groups are [...]")
})
router.Run(":8080")
@@ -340,7 +365,7 @@ func main() {
```
```
-ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
+ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
```
### Upload files
@@ -588,44 +613,44 @@ func main() {
::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" "
```
-### Controlling Log output coloring
+### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY.
-Never colorize logs:
+Never colorize logs:
```go
func main() {
// Disable log's color
gin.DisableConsoleColor()
-
+
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
-
+
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
-
+
router.Run(":8080")
}
```
-Always colorize logs:
+Always colorize logs:
```go
func main() {
// Force log's color
gin.ForceConsoleColor()
-
+
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
-
+
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
-
+
router.Run(":8080")
}
```
@@ -667,19 +692,19 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
-
+
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
- }
-
+ }
+
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Example for binding XML (
//
//
- // user
+ // manu
// 123
// )
router.POST("/loginXML", func(c *gin.Context) {
@@ -688,12 +713,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
-
+
if xml.User != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
- }
-
+ }
+
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
@@ -705,12 +730,12 @@ func main() {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
-
+
if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
- }
-
+ }
+
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
@@ -807,7 +832,7 @@ $ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
-{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%
+{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%
```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
@@ -918,7 +943,7 @@ func main() {
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
- c.JSON(400, gin.H{"msg": err})
+ c.JSON(400, gin.H{"msg": err.Error()})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
@@ -1145,7 +1170,7 @@ func main() {
data := gin.H{
"foo": "bar",
}
-
+
//callback is x
// Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data)
@@ -1190,21 +1215,21 @@ This feature is unavailable in Go 1.6 and lower.
```go
func main() {
r := gin.Default()
-
+
// Serves unicode entities
r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{
"html": "Hello, world!",
})
})
-
+
// Serves literal characters
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"html": "Hello, world!",
})
})
-
+
// listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
@@ -1255,6 +1280,7 @@ func main() {
}
reader := response.Body
+ defer reader.Close()
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
@@ -1792,8 +1818,8 @@ func main() {
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
- if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- log.Fatalf("listen: %s\n", err)
+ if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
+ log.Printf("listen: %s\n", err)
}
}()
@@ -1802,7 +1828,7 @@ func main() {
quit := make(chan os.Signal)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
- // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
+ // kill -9 is syscall.SIGKILL but can't be caught, so don't need to add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
@@ -1811,10 +1837,11 @@ func main() {
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
+
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
-
+
log.Println("Server exiting")
}
```
@@ -1974,7 +2001,7 @@ func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This reads c.Request.Body and stores the result into the context.
- if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
+ if errA := c.ShouldBindBodyWith(&objA, binding.Form); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// At this time, it reuses body stored in the context.
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
@@ -1996,6 +2023,61 @@ enough to call binding at once.
can be called by `c.ShouldBind()` multiple times without any damage to
performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
+### Bind form-data request with custom struct and custom tag
+
+```go
+const (
+ customerTag = "url"
+ defaultMemory = 32 << 20
+)
+
+type customerBinding struct {}
+
+func (customerBinding) Name() string {
+ return "form"
+}
+
+func (customerBinding) Bind(req *http.Request, obj interface{}) error {
+ if err := req.ParseForm(); err != nil {
+ return err
+ }
+ if err := req.ParseMultipartForm(defaultMemory); err != nil {
+ if err != http.ErrNotMultipart {
+ return err
+ }
+ }
+ if err := binding.MapFormWithTag(obj, req.Form, customerTag); err != nil {
+ return err
+ }
+ return validate(obj)
+}
+
+func validate(obj interface{}) error {
+ if binding.Validator == nil {
+ return nil
+ }
+ return binding.Validator.ValidateStruct(obj)
+}
+
+// Now we can do this!!!
+// FormA is a external type that we can't modify it's tag
+type FormA struct {
+ FieldA string `url:"field_a"`
+}
+
+func ListHandler(s *Service) func(ctx *gin.Context) {
+ return func(ctx *gin.Context) {
+ var urlBinding = customerBinding{}
+ var opt FormA
+ err := ctx.MustBindWith(&opt, urlBinding)
+ if err != nil {
+ ...
+ }
+ ...
+ }
+}
+```
+
### http2 server push
http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
@@ -2115,6 +2197,73 @@ func main() {
}
```
+## Don't trust all proxies
+
+Gin lets you specify which headers to hold the real client IP (if any),
+as well as specifying which proxies (or direct clients) you trust to
+specify one of these headers.
+
+Use function `SetTrustedProxies()` on your `gin.Engine` to specify network addresses
+or network CIDRs from where clients which their request headers related to client
+IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
+IPv6 CIDRs.
+
+**Attention:** Gin trust all proxies by default if you don't specify a trusted
+proxy using the function above, **this is NOT safe**. At the same time, if you don't
+use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
+then `Context.ClientIP()` will return the remote address directly to avoid some
+unnecessary computation.
+
+```go
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+
+ router := gin.Default()
+ router.SetTrustedProxies([]string{"192.168.1.2"})
+
+ router.GET("/", func(c *gin.Context) {
+ // If the client is 192.168.1.2, use the X-Forwarded-For
+ // header to deduce the original client IP from the trust-
+ // worthy parts of that header.
+ // Otherwise, simply return the direct client IP
+ fmt.Printf("ClientIP: %s\n", c.ClientIP())
+ })
+ router.Run()
+}
+```
+
+**Notice:** If you are using a CDN service, you can set the `Engine.TrustedPlatform`
+to skip TrustedProxies check, it has a higher priority than TrustedProxies.
+Look at the example below:
+```go
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+
+ router := gin.Default()
+ // Use predefined header gin.PlatformXXX
+ 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"
+
+ router.GET("/", func(c *gin.Context) {
+ // If you set TrustedPlatform, ClientIP() will resolve the
+ // corresponding header and return IP directly
+ fmt.Printf("ClientIP: %s\n", c.ClientIP())
+ })
+ router.Run()
+}
+```
## Testing
@@ -2173,3 +2322,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
+
diff --git a/auth.go b/auth.go
index 43ad36f5..4d8a6ce4 100644
--- a/auth.go
+++ b/auth.go
@@ -5,6 +5,7 @@
package gin
import (
+ "crypto/subtle"
"encoding/base64"
"net/http"
"strconv"
@@ -30,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false
}
for _, pair := range a {
- if pair.value == authValue {
+ if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
return pair.user, true
}
}
diff --git a/binding/binding.go b/binding/binding.go
index 57562845..deb71661 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
// +build !nomsgpack
package binding
@@ -48,10 +49,11 @@ type BindingUri interface {
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
-// https://github.com/go-playground/validator/tree/v8.18.2.
+// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
- // If the received type is not a struct, any validation should be skipped and nil must be returned.
+ // If the received type is a slice|array, the validation should be performed travel on every element.
+ // If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
@@ -63,7 +65,7 @@ type StructValidator interface {
}
// Validator is the default validator which implements the StructValidator
-// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
diff --git a/binding/binding_msgpack_test.go b/binding/binding_msgpack_test.go
index 9791a607..04d94079 100644
--- a/binding/binding_msgpack_test.go
+++ b/binding/binding_msgpack_test.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
// +build !nomsgpack
package binding
diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go
index fd227b11..23424470 100644
--- a/binding/binding_nomsgpack.go
+++ b/binding/binding_nomsgpack.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build nomsgpack
// +build nomsgpack
package binding
@@ -46,7 +47,7 @@ type BindingUri interface {
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
-// https://github.com/go-playground/validator/tree/v8.18.2.
+// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
@@ -61,7 +62,7 @@ type StructValidator interface {
}
// Validator is the default validator which implements the StructValidator
-// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
diff --git a/binding/binding_test.go b/binding/binding_test.go
index 4424bab9..5b0ce39d 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -13,14 +13,15 @@ import (
"mime/multipart"
"net/http"
"os"
+ "reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin/testdata/protoexample"
- "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/proto"
)
type appkey struct {
@@ -34,7 +35,7 @@ type QueryTest struct {
}
type FooStruct struct {
- Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
+ Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
}
type FooBarStruct struct {
@@ -180,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`)
}
+func TestBindingJSONSlice(t *testing.T) {
+ EnableDecoderDisallowUnknownFields = true
+ defer func() {
+ EnableDecoderDisallowUnknownFields = false
+ }()
+
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
+ testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
+}
+
func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t,
JSON, "json",
@@ -200,6 +215,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}
+func TestBindingJSONStringMap(t *testing.T) {
+ testBodyBindingStringMap(t, JSON,
+ "/", "/",
+ `{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
+}
+
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
@@ -336,6 +357,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer")
}
+func TestBindingFormStringMap(t *testing.T) {
+ testBodyBindingStringMap(t, Form,
+ "/", "",
+ `foo=bar&hello=world`, "")
+ // Should pick the last value
+ testBodyBindingStringMap(t, Form,
+ "/", "",
+ `foo=something&foo=bar&hello=world`, "")
+}
+
+func TestBindingFormStringSliceMap(t *testing.T) {
+ obj := make(map[string][]string)
+ req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ err := Form.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ target := map[string][]string{
+ "foo": {"something", "bar"},
+ "hello": {"world"},
+ }
+ assert.True(t, reflect.DeepEqual(obj, target))
+
+ objInvalid := make(map[string][]int)
+ req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ err = Form.Bind(req, &objInvalid)
+ assert.Error(t, err)
+}
+
func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/",
@@ -366,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "")
}
+func TestBindingQueryStringMap(t *testing.T) {
+ b := Query
+
+ obj := make(map[string]string)
+ req := requestWithBody("GET", "/?foo=bar&hello=world", "")
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ assert.Equal(t, "bar", obj["foo"])
+ assert.Equal(t, "world", obj["hello"])
+
+ obj = make(map[string]string)
+ req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
+ err = b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ assert.Equal(t, "2", obj["foo"])
+ assert.Equal(t, "world", obj["hello"])
+}
+
func TestBindingXML(t *testing.T) {
testBodyBinding(t,
XML, "xml",
@@ -387,6 +461,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`)
}
+func TestBindingYAMLStringMap(t *testing.T) {
+ // YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
+ testBodyBindingStringMap(t, YAML,
+ "/", "/",
+ `{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
+}
+
func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
@@ -751,7 +832,6 @@ func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, ba
assert.Equal(t, 1, obj.Page)
assert.Equal(t, 2, obj.Size)
assert.Equal(t, "test-appkey", obj.Appkey)
-
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
@@ -1114,6 +1194,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err)
}
+func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+ assert.Equal(t, name, b.Name())
+
+ var obj1 []FooStruct
+ req := requestWithBody("POST", path, body)
+ err := b.Bind(req, &obj1)
+ assert.NoError(t, err)
+
+ var obj2 []FooStruct
+ req = requestWithBody("POST", badPath, badBody)
+ err = JSON.Bind(req, &obj2)
+ assert.Error(t, err)
+}
+
+func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
+ obj := make(map[string]string)
+ req := requestWithBody("POST", path, body)
+ if b.Name() == "form" {
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ }
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.NotNil(t, obj)
+ assert.Len(t, obj, 2)
+ assert.Equal(t, "bar", obj["foo"])
+ assert.Equal(t, "world", obj["hello"])
+
+ if badPath != "" && badBody != "" {
+ obj = make(map[string]string)
+ req = requestWithBody("POST", badPath, badBody)
+ err = b.Bind(req, &obj)
+ assert.Error(t, err)
+ }
+
+ objInt := make(map[string]int)
+ req = requestWithBody("POST", path, body)
+ err = b.Bind(req, &objInt)
+ assert.Error(t, err)
+}
+
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
@@ -1219,6 +1339,13 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
err := b.Bind(req, &obj)
assert.Error(t, err)
+ invalid_obj := FooStruct{}
+ req.Body = ioutil.NopCloser(strings.NewReader(`{"msg":"hello"}`))
+ req.Header.Add("Content-Type", MIMEPROTOBUF)
+ err = b.Bind(req, &invalid_obj)
+ assert.Error(t, err)
+ assert.Equal(t, err.Error(), "obj is not ProtoMessage")
+
obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
diff --git a/binding/default_validator.go b/binding/default_validator.go
index a4c1a7f6..87fc4c66 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -5,7 +5,9 @@
package binding
import (
+ "fmt"
"reflect"
+ "strings"
"sync"
"github.com/go-playground/validator/v10"
@@ -16,28 +18,72 @@ type defaultValidator struct {
validate *validator.Validate
}
+type sliceValidateError []error
+
+// Error concatenates all error elements in sliceValidateError into a single string separated by \n.
+func (err sliceValidateError) Error() string {
+ n := len(err)
+ switch n {
+ case 0:
+ return ""
+ default:
+ var b strings.Builder
+ if err[0] != nil {
+ fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
+ }
+ if n > 1 {
+ for i := 1; i < n; i++ {
+ if err[i] != nil {
+ b.WriteString("\n")
+ fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
+ }
+ }
+ }
+ return b.String()
+ }
+}
+
var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
+ if obj == nil {
+ return nil
+ }
+
value := reflect.ValueOf(obj)
- valueType := value.Kind()
- if valueType == reflect.Ptr {
- valueType = value.Elem().Kind()
- }
- if valueType == reflect.Struct {
- v.lazyinit()
- if err := v.validate.Struct(obj); err != nil {
- return err
+ switch value.Kind() {
+ case reflect.Ptr:
+ return v.ValidateStruct(value.Elem().Interface())
+ case reflect.Struct:
+ return v.validateStruct(obj)
+ case reflect.Slice, reflect.Array:
+ count := value.Len()
+ validateRet := make(sliceValidateError, 0)
+ for i := 0; i < count; i++ {
+ if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
+ validateRet = append(validateRet, err)
+ }
}
+ if len(validateRet) == 0 {
+ return nil
+ }
+ return validateRet
+ default:
+ return nil
}
- return nil
+}
+
+// validateStruct receives struct type
+func (v *defaultValidator) validateStruct(obj interface{}) error {
+ v.lazyinit()
+ return v.validate.Struct(obj)
}
// Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info -
-// https://godoc.org/gopkg.in/go-playground/validator.v8
+// https://pkg.go.dev/github.com/go-playground/validator/v10
func (v *defaultValidator) Engine() interface{} {
v.lazyinit()
return v.validate
diff --git a/binding/default_validator_benchmark_test.go b/binding/default_validator_benchmark_test.go
new file mode 100644
index 00000000..839cf710
--- /dev/null
+++ b/binding/default_validator_benchmark_test.go
@@ -0,0 +1,20 @@
+package binding
+
+import (
+ "errors"
+ "strconv"
+ "testing"
+)
+
+func BenchmarkSliceValidateError(b *testing.B) {
+ const size int = 100
+ for i := 0; i < b.N; i++ {
+ e := make(sliceValidateError, size)
+ for j := 0; j < size; j++ {
+ e[j] = errors.New(strconv.Itoa(j))
+ }
+ if len(e.Error()) == 0 {
+ b.Errorf("error")
+ }
+ }
+}
diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go
new file mode 100644
index 00000000..e9debe59
--- /dev/null
+++ b/binding/default_validator_test.go
@@ -0,0 +1,88 @@
+// Copyright 2020 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestSliceValidateError(t *testing.T) {
+ tests := []struct {
+ name string
+ err sliceValidateError
+ want string
+ }{
+ {"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
+ {"has zero elements", sliceValidateError{}, ""},
+ {"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"},
+ {"has two elements",
+ sliceValidateError{
+ errors.New("first error"),
+ errors.New("second error"),
+ },
+ "[0]: first error\n[1]: second error",
+ },
+ {"has many elements",
+ sliceValidateError{
+ errors.New("first error"),
+ errors.New("second error"),
+ nil,
+ nil,
+ nil,
+ errors.New("last error"),
+ },
+ "[0]: first error\n[1]: second error\n[5]: last error",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.err.Error(); got != tt.want {
+ t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestDefaultValidator(t *testing.T) {
+ type exampleStruct struct {
+ A string `binding:"max=8"`
+ B int `binding:"gt=0"`
+ }
+ tests := []struct {
+ name string
+ v *defaultValidator
+ obj interface{}
+ wantErr bool
+ }{
+ {"validate nil obj", &defaultValidator{}, nil, false},
+ {"validate int obj", &defaultValidator{}, 3, false},
+ {"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
+ {"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
+ {"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
+ {"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
+ {"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
+ {"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
+ {"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
+ {"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
+ {"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
+ {"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
+ {"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
+ {"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
+ t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/binding/form.go b/binding/form.go
index b93c34cf..fa2a6540 100644
--- a/binding/form.go
+++ b/binding/form.go
@@ -5,6 +5,7 @@
package binding
import (
+ "errors"
"net/http"
)
@@ -22,10 +23,8 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
- if err := req.ParseMultipartForm(defaultMemory); err != nil {
- if err != http.ErrNotMultipart {
- return err
- }
+ if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
+ return err
}
if err := mapForm(obj, req.Form); err != nil {
return err
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index f0913ea5..fa7ad1bc 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -16,9 +16,17 @@ import (
"github.com/gin-gonic/gin/internal/json"
)
-var errUnknownType = errors.New("unknown type")
+var (
+ errUnknownType = errors.New("unknown type")
-func mapUri(ptr interface{}, m map[string][]string) error {
+ // ErrConvertMapStringSlice can not covert to map[string][]string
+ ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
+
+ // ErrConvertToMapString can not convert to map[string]string
+ ErrConvertToMapString = errors.New("can not convert to map of strings")
+)
+
+func mapURI(ptr interface{}, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri")
}
@@ -26,15 +34,34 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return mapFormByTag(ptr, form, "form")
}
+func MapFormWithTag(ptr interface{}, form map[string][]string, tag string) error {
+ return mapFormByTag(ptr, form, tag)
+}
+
var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
+ // Check if ptr is a map
+ ptrVal := reflect.ValueOf(ptr)
+ var pointed interface{}
+ if ptrVal.Kind() == reflect.Ptr {
+ ptrVal = ptrVal.Elem()
+ pointed = ptrVal.Interface()
+ }
+ if ptrVal.Kind() == reflect.Map &&
+ ptrVal.Type().Key().Kind() == reflect.String {
+ if pointed != nil {
+ ptr = pointed
+ }
+ return setFormMap(ptr, form)
+ }
+
return mappingByPtr(ptr, formSource(form), tag)
}
// setter tries to set value on a walking by fields of a struct
type setter interface {
- TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
+ TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
}
type formSource map[string][]string
@@ -42,7 +69,7 @@ type formSource map[string][]string
var _ setter = formSource(nil)
// TrySet tries to set a value by request's form source (like map[string][]string)
-func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
+func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {
return setByForm(value, field, form, tagValue, opt)
}
@@ -56,7 +83,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
return false, nil
}
- var vKind = value.Kind()
+ vKind := value.Kind()
if vKind == reflect.Ptr {
var isNew bool
@@ -65,14 +92,14 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
isNew = true
vPtr = reflect.New(value.Type().Elem())
}
- isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
+ isSet, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil {
return false, err
}
- if isNew && isSetted {
+ if isNew && isSet {
value.Set(vPtr)
}
- return isSetted, nil
+ return isSet, nil
}
if vKind != reflect.Struct || !field.Anonymous {
@@ -88,19 +115,19 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
if vKind == reflect.Struct {
tValue := value.Type()
- var isSetted bool
+ var isSet bool
for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
- ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
+ ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil {
return false, err
}
- isSetted = isSetted || ok
+ isSet = isSet || ok
}
- return isSetted, nil
+ return isSet, nil
}
return false, nil
}
@@ -137,7 +164,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
return setter.TrySet(value, field, tagValue, setOpt)
}
-func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
+func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
return false, nil
@@ -183,7 +210,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case reflect.Int64:
switch value.Interface().(type) {
case time.Duration:
- return setTimeDuration(val, value, field)
+ return setTimeDuration(val, value)
}
return setIntField(val, 64, value)
case reflect.Uint:
@@ -283,7 +310,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
-
}
if val == "" {
@@ -333,7 +359,7 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
return nil
}
-func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
+func setTimeDuration(val string, value reflect.Value) error {
d, err := time.ParseDuration(val)
if err != nil {
return err
@@ -349,3 +375,29 @@ func head(str, sep string) (head string, tail string) {
}
return str[:idx], str[idx+len(sep):]
}
+
+func setFormMap(ptr interface{}, form map[string][]string) error {
+ el := reflect.TypeOf(ptr).Elem()
+
+ if el.Kind() == reflect.Slice {
+ ptrMap, ok := ptr.(map[string][]string)
+ if !ok {
+ return ErrConvertMapStringSlice
+ }
+ for k, v := range form {
+ ptrMap[k] = v
+ }
+
+ return nil
+ }
+
+ ptrMap, ok := ptr.(map[string]string)
+ if !ok {
+ return ErrConvertToMapString
+ }
+ for k, v := range form {
+ ptrMap[k] = v[len(v)-1] // pick last
+ }
+
+ return nil
+}
diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go
index 2675d46b..516554eb 100644
--- a/binding/form_mapping_test.go
+++ b/binding/form_mapping_test.go
@@ -131,7 +131,7 @@ func TestMappingURI(t *testing.T) {
var s struct {
F int `uri:"field"`
}
- err := mapUri(&s, map[string][]string{"field": {"6"}})
+ err := mapURI(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
@@ -145,6 +145,15 @@ func TestMappingForm(t *testing.T) {
assert.Equal(t, int(6), s.F)
}
+func TestMapFormWithTag(t *testing.T) {
+ var s struct {
+ F int `externalTag:"field"`
+ }
+ err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
+ assert.NoError(t, err)
+ assert.Equal(t, int(6), s.F)
+}
+
func TestMappingTime(t *testing.T) {
var s struct {
Time time.Time
diff --git a/binding/header.go b/binding/header.go
index 179ce4ea..b99302af 100644
--- a/binding/header.go
+++ b/binding/header.go
@@ -29,6 +29,6 @@ type headerSource map[string][]string
var _ setter = headerSource(nil)
-func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
+func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (bool, error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}
diff --git a/binding/json.go b/binding/json.go
index d62e0705..45aaa494 100644
--- a/binding/json.go
+++ b/binding/json.go
@@ -6,7 +6,7 @@ package binding
import (
"bytes"
- "fmt"
+ "errors"
"io"
"net/http"
@@ -32,7 +32,7 @@ func (jsonBinding) Name() string {
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
- return fmt.Errorf("invalid request")
+ return errors.New("invalid request")
}
return decodeJSON(req.Body, obj)
}
diff --git a/binding/json_test.go b/binding/json_test.go
index cae4cccc..fbd5c527 100644
--- a/binding/json_test.go
+++ b/binding/json_test.go
@@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
+
+func TestJSONBindingBindBodyMap(t *testing.T) {
+ s := make(map[string]string)
+ err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
+ require.NoError(t, err)
+ assert.Len(t, s, 2)
+ assert.Equal(t, "FOO", s["foo"])
+ assert.Equal(t, "world", s["hello"])
+}
diff --git a/binding/msgpack.go b/binding/msgpack.go
index a5bc2ad2..2a442996 100644
--- a/binding/msgpack.go
+++ b/binding/msgpack.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
// +build !nomsgpack
package binding
diff --git a/binding/msgpack_test.go b/binding/msgpack_test.go
index 296d3eb1..75600ba8 100644
--- a/binding/msgpack_test.go
+++ b/binding/msgpack_test.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
// +build !nomsgpack
package binding
diff --git a/binding/multipart_form_mapping.go b/binding/multipart_form_mapping.go
index f85a1aa6..c4d7ed74 100644
--- a/binding/multipart_form_mapping.go
+++ b/binding/multipart_form_mapping.go
@@ -15,8 +15,16 @@ type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
+var (
+ // ErrMultiFileHeader multipart.FileHeader invalid
+ ErrMultiFileHeader = errors.New("unsupported field type for multipart.FileHeader")
+
+ // ErrMultiFileHeaderLenInvalid array for []*multipart.FileHeader len invalid
+ ErrMultiFileHeaderLenInvalid = errors.New("unsupported len of array for []*multipart.FileHeader")
+)
+
// TrySet tries to set a value by the multipart request with the binding a form file
-func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
+func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (bool, error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
@@ -24,7 +32,7 @@ func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
-func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
+func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
@@ -40,26 +48,26 @@ func setByMultipartFormFile(value reflect.Value, field reflect.StructField, file
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
- isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
- if err != nil || !isSetted {
- return isSetted, err
+ isSet, err = setArrayOfMultipartFormFiles(slice, field, files)
+ if err != nil || !isSet {
+ return isSet, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
- return false, errors.New("unsupported field type for multipart.FileHeader")
+ return false, ErrMultiFileHeader
}
-func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
+func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
if value.Len() != len(files) {
- return false, errors.New("unsupported len of array for []*multipart.FileHeader")
+ return false, ErrMultiFileHeaderLenInvalid
}
for i := range files {
- setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
- if err != nil || !setted {
- return setted, err
+ set, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
+ if err != nil || !set {
+ return set, err
}
}
return true, nil
diff --git a/binding/multipart_form_mapping_test.go b/binding/multipart_form_mapping_test.go
index 4c75d1fe..4aaa60be 100644
--- a/binding/multipart_form_mapping_test.go
+++ b/binding/multipart_form_mapping_test.go
@@ -124,7 +124,7 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
- // assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
+ assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open()
assert.NoError(t, err)
diff --git a/binding/protobuf.go b/binding/protobuf.go
index f9ece928..a4e47153 100644
--- a/binding/protobuf.go
+++ b/binding/protobuf.go
@@ -5,10 +5,11 @@
package binding
import (
+ "errors"
"io/ioutil"
"net/http"
- "github.com/golang/protobuf/proto"
+ "google.golang.org/protobuf/proto"
)
type protobufBinding struct{}
@@ -26,7 +27,11 @@ func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
}
func (protobufBinding) BindBody(body []byte, obj interface{}) error {
- if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
+ msg, ok := obj.(proto.Message)
+ if !ok {
+ return errors.New("obj is not ProtoMessage")
+ }
+ if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but util now we can't add
diff --git a/binding/uri.go b/binding/uri.go
index f91ec381..a3c0df51 100644
--- a/binding/uri.go
+++ b/binding/uri.go
@@ -11,7 +11,7 @@ func (uriBinding) Name() string {
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
- if err := mapUri(obj, m); err != nil {
+ if err := mapURI(obj, m); err != nil {
return err
}
return validate(obj)
diff --git a/context.go b/context.go
index 95b1807d..58f38c88 100644
--- a/context.go
+++ b/context.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"io/ioutil"
+ "log"
"math"
"mime/multipart"
"net"
@@ -39,7 +40,8 @@ const (
// BodyBytesKey indicates a default body bytes key.
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
-const abortIndex int8 = math.MaxInt8 / 2
+// abortIndex represents a typical value used in abort functions.
+const abortIndex int8 = math.MaxInt8 >> 1
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
@@ -53,8 +55,9 @@ type Context struct {
index int8
fullPath string
- engine *Engine
- params *Params
+ engine *Engine
+ params *Params
+ skippedNodes *[]skippedNode
// This mutex protect Keys map
mu sync.RWMutex
@@ -86,17 +89,18 @@ type Context struct {
func (c *Context) reset() {
c.Writer = &c.writermem
- c.Params = c.Params[0:0]
+ c.Params = c.Params[:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
- c.Errors = c.Errors[0:0]
+ c.Errors = c.Errors[:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
- *c.params = (*c.params)[0:0]
+ *c.params = (*c.params)[:0]
+ *c.skippedNodes = (*c.skippedNodes)[:0]
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
@@ -218,7 +222,8 @@ func (c *Context) Error(err error) *Error {
panic("err is nil")
}
- parsedError, ok := err.(*Error)
+ var parsedError *Error
+ ok := errors.As(err, &parsedError)
if !ok {
parsedError = &Error{
Err: err,
@@ -295,6 +300,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
return
}
+// GetUint returns the value associated with the key as an unsigned integer.
+func (c *Context) GetUint(key string) (ui uint) {
+ if val, ok := c.Get(key); ok && val != nil {
+ ui, _ = val.(uint)
+ }
+ return
+}
+
+// GetUint64 returns the value associated with the key as an unsigned integer.
+func (c *Context) GetUint64(key string) (ui64 uint64) {
+ if val, ok := c.Get(key); ok && val != nil {
+ ui64, _ = val.(uint64)
+ }
+ return
+}
+
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
@@ -365,6 +386,15 @@ func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
+// AddParam adds param to context and
+// replaces path param key with given value for e2e testing purposes
+// Example Route: "/user/:id"
+// AddParam("id", 1)
+// Result: "/user/1"
+func (c *Context) AddParam(key, value string) {
+ c.Params = append(c.Params, Param{Key: key, Value: value})
+}
+
// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
@@ -373,9 +403,9 @@ func (c *Context) Param(key string) string {
// c.Query("name") == "Manu"
// c.Query("value") == ""
// c.Query("wtf") == ""
-func (c *Context) Query(key string) string {
- value, _ := c.GetQuery(key)
- return value
+func (c *Context) Query(key string) (value string) {
+ value, _ = c.GetQuery(key)
+ return
}
// DefaultQuery returns the keyed url query value if it exists,
@@ -409,9 +439,9 @@ func (c *Context) GetQuery(key string) (string, bool) {
// QueryArray returns a slice of strings for a given query key.
// The length of the slice depends on the number of params with the given key.
-func (c *Context) QueryArray(key string) []string {
- values, _ := c.GetQueryArray(key)
- return values
+func (c *Context) QueryArray(key string) (values []string) {
+ values, _ = c.GetQueryArray(key)
+ return
}
func (c *Context) initQueryCache() {
@@ -426,18 +456,16 @@ func (c *Context) initQueryCache() {
// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
-func (c *Context) GetQueryArray(key string) ([]string, bool) {
+func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
c.initQueryCache()
- if values, ok := c.queryCache[key]; ok && len(values) > 0 {
- return values, true
- }
- return []string{}, false
+ values, ok = c.queryCache[key]
+ return
}
// QueryMap returns a map for a given query key.
-func (c *Context) QueryMap(key string) map[string]string {
- dicts, _ := c.GetQueryMap(key)
- return dicts
+func (c *Context) QueryMap(key string) (dicts map[string]string) {
+ dicts, _ = c.GetQueryMap(key)
+ return
}
// GetQueryMap returns a map for a given query key, plus a boolean value
@@ -449,9 +477,9 @@ func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
-func (c *Context) PostForm(key string) string {
- value, _ := c.GetPostForm(key)
- return value
+func (c *Context) PostForm(key string) (value string) {
+ value, _ = c.GetPostForm(key)
+ return
}
// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form
@@ -480,9 +508,9 @@ func (c *Context) GetPostForm(key string) (string, bool) {
// PostFormArray returns a slice of strings for a given form key.
// The length of the slice depends on the number of params with the given key.
-func (c *Context) PostFormArray(key string) []string {
- values, _ := c.GetPostFormArray(key)
- return values
+func (c *Context) PostFormArray(key string) (values []string) {
+ values, _ = c.GetPostFormArray(key)
+ return
}
func (c *Context) initFormCache() {
@@ -490,7 +518,7 @@ func (c *Context) initFormCache() {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
- if err != http.ErrNotMultipart {
+ if !errors.Is(err, http.ErrNotMultipart) {
debugPrint("error on parse multipart form array: %v", err)
}
}
@@ -500,18 +528,16 @@ func (c *Context) initFormCache() {
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
-func (c *Context) GetPostFormArray(key string) ([]string, bool) {
+func (c *Context) GetPostFormArray(key string) (values []string, ok bool) {
c.initFormCache()
- if values := c.formCache[key]; len(values) > 0 {
- return values, true
- }
- return []string{}, false
+ values, ok = c.formCache[key]
+ return
}
// PostFormMap returns a map for a given form key.
-func (c *Context) PostFormMap(key string) map[string]string {
- dicts, _ := c.GetPostFormMap(key)
- return dicts
+func (c *Context) PostFormMap(key string) (dicts map[string]string) {
+ dicts, _ = c.GetPostFormMap(key)
+ return
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
@@ -709,32 +735,91 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
return bb.BindBody(body, obj)
}
-// ClientIP implements a best effort algorithm to return the real client IP, it parses
-// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
-// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
+// ClientIP implements one best effort algorithm to return the real client IP.
+// It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
+// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
+// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
+// the remote IP (coming form Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string {
- if c.engine.ForwardedByClientIP {
- clientIP := c.requestHeader("X-Forwarded-For")
- clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
- if clientIP == "" {
- clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
- }
- if clientIP != "" {
- return clientIP
+ // Check if we're running on a trusted platform, continue running backwards if error
+ if c.engine.TrustedPlatform != "" {
+ // Developers can define their own header of Trusted Platform or use predefined constants
+ if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
+ return addr
}
}
+ // Legacy "AppEngine" flag
if c.engine.AppEngine {
+ log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine' instead.`)
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
- if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
- return ip
+ remoteIP, trusted := c.RemoteIP()
+ if remoteIP == nil {
+ return ""
}
- return ""
+ if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
+ for _, headerName := range c.engine.RemoteIPHeaders {
+ ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
+ if valid {
+ return ip
+ }
+ }
+ }
+ return remoteIP.String()
+}
+
+func (e *Engine) isTrustedProxy(ip net.IP) bool {
+ if e.trustedCIDRs != nil {
+ for _, cidr := range e.trustedCIDRs {
+ if cidr.Contains(ip) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
+// It also checks if the remoteIP is a trusted proxy or not.
+// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
+// defined by Engine.SetTrustedProxies()
+func (c *Context) RemoteIP() (net.IP, bool) {
+ ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
+ if err != nil {
+ return nil, false
+ }
+ remoteIP := net.ParseIP(ip)
+ if remoteIP == nil {
+ return nil, false
+ }
+
+ return remoteIP, c.engine.isTrustedProxy(remoteIP)
+}
+
+func (e *Engine) validateHeader(header string) (clientIP string, valid bool) {
+ if header == "" {
+ return "", false
+ }
+ items := strings.Split(header, ",")
+ for i := len(items) - 1; i >= 0; i-- {
+ ipStr := strings.TrimSpace(items[i])
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return "", false
+ }
+
+ // X-Forwarded-For is appended by proxy
+ // Check IPs in reverse order and stop when find untrusted proxy
+ if (i == 0) || (!e.isTrustedProxy(ip)) {
+ return ipStr, true
+ }
+ }
+ return
}
// ContentType returns the Content-Type header of the request.
@@ -875,7 +960,7 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
}
// JSONP serializes the given struct as JSON into the response body.
-// It add padding to response body to request data from a server residing in a different domain than the client.
+// It adds padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "")
@@ -952,12 +1037,12 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri
})
}
-// File writes the specified file into the body stream in a efficient way.
+// File writes the specified file into the body stream in an efficient way.
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
-// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way.
+// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
defer func(old string) {
c.Request.URL.Path = old
@@ -971,7 +1056,7 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
// 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) {
- c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+ c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath)
}
@@ -1081,22 +1166,28 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
-// Deadline always returns that there is no deadline (ok==false),
-// maybe you want to use Request.Context().Deadline() instead.
+// Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
- return
+ if c.Request == nil || c.Request.Context() == nil {
+ return
+ }
+ return c.Request.Context().Deadline()
}
-// Done always returns nil (chan which will wait forever),
-// if you want to abort your work when the connection was closed
-// you should use Request.Context().Done() instead.
+// Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} {
- return nil
+ if c.Request == nil || c.Request.Context() == nil {
+ return nil
+ }
+ return c.Request.Context().Done()
}
-// Err always returns nil, maybe you want to use Request.Context().Err() instead.
+// Err returns nil when c.Request has no Context.
func (c *Context) Err() error {
- return nil
+ if c.Request == nil || c.Request.Context() == nil {
+ return nil
+ }
+ return c.Request.Context().Err()
}
// Value returns the value associated with this context for key, or nil
@@ -1107,8 +1198,12 @@ func (c *Context) Value(key interface{}) interface{} {
return c.Request
}
if keyAsString, ok := key.(string); ok {
- val, _ := c.Get(keyAsString)
- return val
+ if val, exists := c.Get(keyAsString); exists {
+ return val
+ }
}
- return nil
+ if c.Request == nil || c.Request.Context() == nil {
+ return nil
+ }
+ return c.Request.Context().Value(key)
}
diff --git a/context_1.16_test.go b/context_1.16_test.go
new file mode 100644
index 00000000..053e6c5a
--- /dev/null
+++ b/context_1.16_test.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+//go:build !go1.17
+// +build !go1.17
+
+package gin
+
+import (
+ "bytes"
+ "mime/multipart"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestContextFormFileFailed16(t *testing.T) {
+ buf := new(bytes.Buffer)
+ mw := multipart.NewWriter(buf)
+ mw.Close()
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+ c.engine.MaxMultipartMemory = 8 << 20
+ f, err := c.FormFile("file")
+ assert.Error(t, err)
+ assert.Nil(t, f)
+}
diff --git a/context_1.17_test.go b/context_1.17_test.go
new file mode 100644
index 00000000..431d54c7
--- /dev/null
+++ b/context_1.17_test.go
@@ -0,0 +1,33 @@
+// Copyright 2021 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+//go:build go1.17
+// +build go1.17
+
+package gin
+
+import (
+ "bytes"
+ "mime/multipart"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestContextFormFileFailed17(t *testing.T) {
+ buf := new(bytes.Buffer)
+ mw := multipart.NewWriter(buf)
+ mw.Close()
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+ c.engine.MaxMultipartMemory = 8 << 20
+ assert.Panics(t, func() {
+ f, err := c.FormFile("file")
+ assert.Error(t, err)
+ assert.Nil(t, f)
+ })
+}
diff --git a/context_appengine.go b/context_appengine.go
index 38c189a0..8bf93896 100644
--- a/context_appengine.go
+++ b/context_appengine.go
@@ -1,11 +1,12 @@
-// +build appengine
-
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build appengine
+// +build appengine
+
package gin
func init() {
- defaultAppEngine = true
+ defaultPlatform = PlatformGoogleAppEngine
}
diff --git a/context_test.go b/context_test.go
index 1a5a3c5f..c286c0f4 100644
--- a/context_test.go
+++ b/context_test.go
@@ -23,10 +23,9 @@ import (
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
- "github.com/golang/protobuf/proto"
- "github.com/stretchr/testify/assert"
-
testdata "github.com/gin-gonic/gin/testdata/protoexample"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/proto"
)
var _ context.Context = &Context{}
@@ -87,19 +86,6 @@ func TestContextFormFile(t *testing.T) {
assert.NoError(t, c.SaveUploadedFile(f, "test"))
}
-func TestContextFormFileFailed(t *testing.T) {
- buf := new(bytes.Buffer)
- mw := multipart.NewWriter(buf)
- mw.Close()
- c, _ := CreateTestContext(httptest.NewRecorder())
- c.Request, _ = http.NewRequest("POST", "/", nil)
- c.Request.Header.Set("Content-Type", mw.FormDataContentType())
- c.engine.MaxMultipartMemory = 8 << 20
- f, err := c.FormFile("file")
- assert.Error(t, err)
- assert.Nil(t, f)
-}
-
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
@@ -234,7 +220,6 @@ func TestContextSetGetValues(t *testing.T) {
assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2))
assert.Exactly(t, c.MustGet("float64").(float64), 4.2)
assert.Exactly(t, c.MustGet("intInterface").(int), 1)
-
}
func TestContextGetString(t *testing.T) {
@@ -261,6 +246,18 @@ func TestContextGetInt64(t *testing.T) {
assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
}
+func TestContextGetUint(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("uint", uint(1))
+ assert.Equal(t, uint(1), c.GetUint("uint"))
+}
+
+func TestContextGetUint64(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("uint64", uint64(18446744073709551615))
+ assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64"))
+}
+
func TestContextGetFloat64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2)
@@ -288,7 +285,7 @@ func TestContextGetStringSlice(t *testing.T) {
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
- var m = make(map[string]interface{})
+ m := make(map[string]interface{})
m["foo"] = 1
c.Set("map", m)
@@ -298,7 +295,7 @@ func TestContextGetStringMap(t *testing.T) {
func TestContextGetStringMapString(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
- var m = make(map[string]string)
+ m := make(map[string]string)
m["foo"] = "bar"
c.Set("map", m)
@@ -308,7 +305,7 @@ func TestContextGetStringMapString(t *testing.T) {
func TestContextGetStringMapStringSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
- var m = make(map[string][]string)
+ m := make(map[string][]string)
m["foo"] = []string{"foo"}
c.Set("map", m)
@@ -357,15 +354,12 @@ func TestContextHandlerNames(t *testing.T) {
}
func handlerNameTest(c *Context) {
-
}
func handlerNameTest2(c *Context) {
-
}
var handlerTest HandlerFunc = func(c *Context) {
-
}
func TestContextHandler(t *testing.T) {
@@ -647,8 +641,7 @@ func TestContextBodyAllowedForStatus(t *testing.T) {
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
}
-type TestPanicRender struct {
-}
+type TestPanicRender struct{}
func (*TestPanicRender) Render(http.ResponseWriter) error {
return errors.New("TestPanicRender")
@@ -1006,7 +999,9 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
- assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
+ // 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"))
}
func TestContextRenderFileFromFS(t *testing.T) {
@@ -1018,7 +1013,9 @@ func TestContextRenderFileFromFS(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
- assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
+ // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
+ // else, Content-Type='text/x-go; charset=utf-8'
+ assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path)
}
@@ -1032,7 +1029,7 @@ func TestContextRenderAttachment(t *testing.T) {
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
- assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition"))
+ assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
}
// TestContextRenderYAML tests that the response is serialized as YAML
@@ -1270,7 +1267,7 @@ func TestContextIsAborted(t *testing.T) {
assert.True(t, c.IsAborted())
}
-// TestContextData tests that the response can be written from `bytesting`
+// TestContextData tests that the response can be written from `bytestring`
// with specified MIME type
func TestContextAbortWithStatus(t *testing.T) {
w := httptest.NewRecorder()
@@ -1313,7 +1310,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
- assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
+ assert.Equal(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
}
func TestContextError(t *testing.T) {
@@ -1379,12 +1376,11 @@ func TestContextAbortWithError(t *testing.T) {
func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
+ resetContextForClientIPTests(c)
- c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
- 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.RemoteAddr = " 40.40.40.40:42123 "
-
+ // Legacy tests (validating that the defaults don't break the
+ // (insecure!) old behaviour)
assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
@@ -1395,7 +1391,7 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP")
- c.engine.AppEngine = true
+ c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
@@ -1404,6 +1400,107 @@ func TestContextClientIP(t *testing.T) {
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
+
+ // Tests exercising the TrustedProxies functionality
+ resetContextForClientIPTests(c)
+
+ // No trusted proxies
+ _ = c.engine.SetTrustedProxies([]string{})
+ c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Disabled TrustedProxies feature
+ _ = c.engine.SetTrustedProxies(nil)
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Last proxy is trusted, but the RemoteAddr is not
+ _ = c.engine.SetTrustedProxies([]string{"30.30.30.30"})
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Only trust RemoteAddr
+ _ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
+ assert.Equal(t, "30.30.30.30", c.ClientIP())
+
+ // All steps are trusted
+ _ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30", "20.20.20.20"})
+ assert.Equal(t, "20.20.20.20", c.ClientIP())
+
+ // Use CIDR
+ _ = c.engine.SetTrustedProxies([]string{"40.40.25.25/16", "30.30.30.30"})
+ assert.Equal(t, "20.20.20.20", c.ClientIP())
+
+ // Use hostname that resolves to all the proxies
+ _ = c.engine.SetTrustedProxies([]string{"foo"})
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Use hostname that returns an error
+ _ = c.engine.SetTrustedProxies([]string{"bar"})
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // X-Forwarded-For has a non-IP element
+ _ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
+ c.Request.Header.Set("X-Forwarded-For", " blah ")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Result from LookupHost has non-IP element. This should never
+ // happen, but we should test it to make sure we handle it
+ // gracefully.
+ _ = c.engine.SetTrustedProxies([]string{"baz"})
+ c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ _ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
+ c.Request.Header.Del("X-Forwarded-For")
+ c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
+ assert.Equal(t, "10.10.10.10", c.ClientIP())
+
+ c.engine.RemoteIPHeaders = []string{}
+ c.engine.TrustedPlatform = PlatformGoogleAppEngine
+ assert.Equal(t, "50.50.50.50", c.ClientIP())
+
+ // Use custom TrustedPlatform header
+ c.engine.TrustedPlatform = "X-CDN-IP"
+ c.Request.Header.Set("X-CDN-IP", "80.80.80.80")
+ assert.Equal(t, "80.80.80.80", c.ClientIP())
+ // wrong header
+ c.engine.TrustedPlatform = "X-Wrong-Header"
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ c.Request.Header.Del("X-CDN-IP")
+ // TrustedPlatform is empty
+ c.engine.TrustedPlatform = ""
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // Test the legacy flag
+ c.engine.AppEngine = true
+ assert.Equal(t, "50.50.50.50", c.ClientIP())
+ c.engine.AppEngine = false
+ c.engine.TrustedPlatform = PlatformGoogleAppEngine
+
+ c.Request.Header.Del("X-Appengine-Remote-Addr")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ c.engine.TrustedPlatform = PlatformCloudflare
+ assert.Equal(t, "60.60.60.60", c.ClientIP())
+
+ c.Request.Header.Del("CF-Connecting-IP")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ c.engine.TrustedPlatform = ""
+
+ // no port
+ c.Request.RemoteAddr = "50.50.50.50"
+ assert.Empty(t, c.ClientIP())
+}
+
+func resetContextForClientIPTests(c *Context) {
+ c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
+ 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.RemoteAddr = " 40.40.40.40:42123 "
+ c.engine.TrustedPlatform = ""
+ c.engine.AppEngine = false
}
func TestContextContentType(t *testing.T) {
@@ -1445,6 +1542,7 @@ func TestContextBindWithJSON(t *testing.T) {
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
+
func TestContextBindWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@@ -1948,3 +2046,120 @@ func TestContextWithKeysMutex(t *testing.T) {
assert.Nil(t, value)
assert.False(t, err)
}
+
+func TestRemoteIPFail(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.Request.RemoteAddr = "[:::]:80"
+ ip, trust := c.RemoteIP()
+ assert.Nil(t, ip)
+ assert.False(t, trust)
+}
+
+func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) {
+ c := &Context{}
+ deadline, ok := c.Deadline()
+ assert.Zero(t, deadline)
+ assert.False(t, ok)
+
+ c2 := &Context{}
+ c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
+ d := time.Now().Add(time.Second)
+ ctx, cancel := context.WithDeadline(context.Background(), d)
+ defer cancel()
+ c2.Request = c2.Request.WithContext(ctx)
+ deadline, ok = c2.Deadline()
+ assert.Equal(t, d, deadline)
+ assert.True(t, ok)
+}
+
+func TestContextWithFallbackDoneFromRequestContext(t *testing.T) {
+ c := &Context{}
+ assert.Nil(t, c.Done())
+
+ c2 := &Context{}
+ c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
+ ctx, cancel := context.WithCancel(context.Background())
+ c2.Request = c2.Request.WithContext(ctx)
+ cancel()
+ assert.NotNil(t, <-c2.Done())
+}
+
+func TestContextWithFallbackErrFromRequestContext(t *testing.T) {
+ c := &Context{}
+ assert.Nil(t, c.Err())
+
+ c2 := &Context{}
+ c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
+ ctx, cancel := context.WithCancel(context.Background())
+ c2.Request = c2.Request.WithContext(ctx)
+ cancel()
+
+ assert.EqualError(t, c2.Err(), context.Canceled.Error())
+}
+
+type contextKey string
+
+func TestContextWithFallbackValueFromRequestContext(t *testing.T) {
+ tests := []struct {
+ name string
+ getContextAndKey func() (*Context, interface{})
+ value interface{}
+ }{
+ {
+ name: "c with struct context key",
+ getContextAndKey: func() (*Context, interface{}) {
+ var key struct{}
+ c := &Context{}
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.Request = c.Request.WithContext(context.WithValue(context.TODO(), key, "value"))
+ return c, key
+ },
+ value: "value",
+ },
+ {
+ name: "c with string context key",
+ getContextAndKey: func() (*Context, interface{}) {
+ c := &Context{}
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ c.Request = c.Request.WithContext(context.WithValue(context.TODO(), contextKey("key"), "value"))
+ return c, contextKey("key")
+ },
+ value: "value",
+ },
+ {
+ name: "c with nil http.Request",
+ getContextAndKey: func() (*Context, interface{}) {
+ c := &Context{}
+ return c, "key"
+ },
+ value: nil,
+ },
+ {
+ name: "c with nil http.Request.Context()",
+ getContextAndKey: func() (*Context, interface{}) {
+ c := &Context{}
+ c.Request, _ = http.NewRequest("POST", "/", nil)
+ return c, "key"
+ },
+ value: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c, key := tt.getContextAndKey()
+ assert.Equal(t, tt.value, c.Value(key))
+ })
+ }
+}
+
+func TestContextAddParam(t *testing.T) {
+ c := &Context{}
+ id := "id"
+ value := "1"
+ c.AddParam(id, value)
+
+ v, ok := c.Params.Get(id)
+ assert.Equal(t, ok, true)
+ assert.Equal(t, value, v)
+}
diff --git a/debug.go b/debug.go
index c66ca440..ed313868 100644
--- a/debug.go
+++ b/debug.go
@@ -12,7 +12,7 @@ import (
"strings"
)
-const ginSupportMinGoVer = 10
+const ginSupportMinGoVer = 13
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@@ -67,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
- debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.
+ debugPrint(`[WARNING] Now Gin requires Go 1.13+.
`)
}
@@ -95,9 +95,7 @@ at initialization. ie. before any route is registered or the router is listening
}
func debugPrintError(err error) {
- if err != nil {
- if IsDebugging() {
- fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
- }
+ if err != nil && IsDebugging() {
+ fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
diff --git a/debug_test.go b/debug_test.go
index d8cd5d1a..05509992 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -104,7 +104,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
- assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
+ assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.13+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
diff --git a/errors.go b/errors.go
index 9a317992..3418cbc8 100644
--- a/errors.go
+++ b/errors.go
@@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0
}
+// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
+func (msg *Error) Unwrap() error {
+ return msg.Err
+}
+
// ByType returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
@@ -117,7 +122,7 @@ func (a errorMsgs) Last() *Error {
return nil
}
-// Errors returns an array will all the error messages.
+// Errors returns an array with all the error messages.
// Example:
// c.Error(errors.New("first"))
// c.Error(errors.New("second"))
diff --git a/errors_test.go b/errors_test.go
index 6aae1c10..9a17d859 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -6,6 +6,7 @@ package gin
import (
"errors"
+ "fmt"
"testing"
"github.com/gin-gonic/gin/internal/json"
@@ -104,3 +105,24 @@ Error #03: third
assert.Nil(t, errs.JSON())
assert.Empty(t, errs.String())
}
+
+type TestErr string
+
+func (e TestErr) Error() string { return string(e) }
+
+// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
+// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13.
+func TestErrorUnwrap(t *testing.T) {
+ innerErr := TestErr("some error")
+
+ // 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
+ err := fmt.Errorf("wrapped: %w", &Error{
+ Err: innerErr,
+ Type: ErrorTypeAny,
+ })
+
+ // check that 'errors.Is()' and 'errors.As()' behave as expected :
+ assert.True(t, errors.Is(err, innerErr))
+ var testErr TestErr
+ assert.True(t, errors.As(err, &testErr))
+}
diff --git a/fs.go b/fs.go
index 007d9b75..e5f3d602 100644
--- a/fs.go
+++ b/fs.go
@@ -17,7 +17,7 @@ type neuteredReaddirFile struct {
http.File
}
-// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
+// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
// in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files.
diff --git a/gin.go b/gin.go
index 004ab1f7..51b53164 100644
--- a/gin.go
+++ b/gin.go
@@ -11,6 +11,8 @@ import (
"net/http"
"os"
"path"
+ "reflect"
+ "strings"
"sync"
"github.com/gin-gonic/gin/internal/bytesconv"
@@ -24,7 +26,9 @@ var (
default405Body = []byte("405 method not allowed")
)
-var defaultAppEngine bool
+var defaultPlatform string
+
+var defaultTrustedCIDRs = []*net.IPNet{{IP: net.IP{0x0, 0x0, 0x0, 0x0}, Mask: net.IPMask{0x0, 0x0, 0x0, 0x0}}} // 0.0.0.0/0
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
@@ -51,6 +55,16 @@ type RouteInfo struct {
// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo
+// Trusted platforms
+const (
+ // When running on Google App Engine. Trust X-Appengine-Remote-Addr
+ // for determining the client's IP
+ PlatformGoogleAppEngine = "X-Appengine-Remote-Addr"
+ // When using Cloudflare's CDN. Trust CF-Connecting-IP for determining
+ // the client's IP
+ PlatformCloudflare = "CF-Connecting-IP"
+)
+
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
@@ -81,9 +95,15 @@ type Engine struct {
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
- ForwardedByClientIP bool
- // #726 #755 If enabled, it will thrust some headers starting with
+ // If enabled, client IP will be parsed from the request's headers that
+ // match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
+ // fetched, it falls back to the IP obtained from
+ // `(*gin.Context).Request.RemoteAddr`.
+ ForwardedByClientIP bool
+
+ // DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.GoogleAppEngine` INSTEAD
+ // #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool
@@ -95,14 +115,24 @@ type Engine struct {
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
- // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
- // method call.
- MaxMultipartMemory int64
-
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
// See the PR #1817 and issue #1644
RemoveExtraSlash bool
+ // List of headers used to obtain the client IP when
+ // `(*gin.Engine).ForwardedByClientIP` is `true` and
+ // `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
+ // network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
+ RemoteIPHeaders []string
+
+ // If set to a constant of value gin.Platform*, trusts the headers set by
+ // that platform, for example to determine the client IP
+ TrustedPlatform string
+
+ // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
+ // method call.
+ MaxMultipartMemory int64
+
delims render.Delims
secureJSONPrefix string
HTMLRender render.HTMLRender
@@ -116,6 +146,9 @@ type Engine struct {
pool sync.Pool
trees methodTrees
maxParams uint16
+ maxSections uint16
+ trustedProxies []string
+ trustedCIDRs []*net.IPNet
}
var _ IRouter = &Engine{}
@@ -141,7 +174,8 @@ func New() *Engine {
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
- AppEngine: defaultAppEngine,
+ RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
+ TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
@@ -149,6 +183,8 @@ func New() *Engine {
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
+ trustedProxies: []string{"0.0.0.0/0"},
+ trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
@@ -167,7 +203,8 @@ func Default() *Engine {
func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, engine.maxParams)
- return &Context{engine: engine, params: &v}
+ skippedNodes := make([]skippedNode, 0, engine.maxSections)
+ return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
// Delims sets template left and right delims and returns a Engine instance.
@@ -230,7 +267,7 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.rebuild404Handlers()
}
-// NoMethod sets the handlers called when... TODO.
+// NoMethod sets the handlers called when Engine.HandleMethodNotAllowed = true.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.noMethod = handlers
engine.rebuild405Handlers()
@@ -285,6 +322,10 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
+
+ if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
+ engine.maxSections = sectionsCount
+ }
}
// Routes returns a slice of registered routes, including some useful information, such as:
@@ -319,12 +360,85 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+ }
+
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
+func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
+ if engine.trustedProxies == nil {
+ return nil, nil
+ }
+
+ cidr := make([]*net.IPNet, 0, len(engine.trustedProxies))
+ for _, trustedProxy := range engine.trustedProxies {
+ if !strings.Contains(trustedProxy, "/") {
+ ip := parseIP(trustedProxy)
+ if ip == nil {
+ return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
+ }
+
+ switch len(ip) {
+ case net.IPv4len:
+ trustedProxy += "/32"
+ case net.IPv6len:
+ trustedProxy += "/128"
+ }
+ }
+ _, cidrNet, err := net.ParseCIDR(trustedProxy)
+ if err != nil {
+ return cidr, err
+ }
+ cidr = append(cidr, cidrNet)
+ }
+ return cidr, nil
+}
+
+// SetTrustedProxies set a list of network origins (IPv4 addresses,
+// IPv4 CIDRs, IPv6 addresses or IPv6 CIDRs) from which to trust
+// request's headers that contain alternative client IP when
+// `(*gin.Engine).ForwardedByClientIP` is `true`. `TrustedProxies`
+// feature is enabled by default, and it also trusts all proxies
+// by default. If you want to disable this feature, use
+// Engine.SetTrustedProxies(nil), then Context.ClientIP() will
+// return the remote address directly.
+func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
+ engine.trustedProxies = trustedProxies
+ return engine.parseTrustedProxies()
+}
+
+// isUnsafeTrustedProxies compares Engine.trustedCIDRs and defaultTrustedCIDRs, it's not safe if equal (returns true)
+func (engine *Engine) isUnsafeTrustedProxies() bool {
+ return reflect.DeepEqual(engine.trustedCIDRs, defaultTrustedCIDRs)
+}
+
+// parseTrustedProxies parse Engine.trustedProxies to Engine.trustedCIDRs
+func (engine *Engine) parseTrustedProxies() error {
+ trustedCIDRs, err := engine.prepareTrustedCIDRs()
+ engine.trustedCIDRs = trustedCIDRs
+ return err
+}
+
+// parseIP parse a string representation of an IP and returns a net.IP with the
+// minimum byte representation or nil if input is invalid.
+func parseIP(ip string) net.IP {
+ parsedIP := net.ParseIP(ip)
+
+ if ipv4 := parsedIP.To4(); ipv4 != nil {
+ // return ip in a 4-byte representation
+ return ipv4
+ }
+
+ // return ip in a 16-byte representation or nil
+ return parsedIP
+}
+
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
@@ -332,6 +446,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving HTTPS on %s\n", addr)
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+ }
+
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
return
}
@@ -343,6 +462,11 @@ func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file)
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+ }
+
listener, err := net.Listen("unix", file)
if err != nil {
return
@@ -361,6 +485,11 @@ func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd)
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+ }
+
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f)
if err != nil {
@@ -376,6 +505,12 @@ func (engine *Engine) RunFd(fd int) (err error) {
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
+ }
+
err = http.Serve(listener, engine)
return
}
@@ -424,7 +559,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
root := t[i].root
// Find route in tree
- value := root.getValue(rPath, c.params, unescape)
+ value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
@@ -435,7 +570,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
c.writermem.WriteHeaderNow()
return
}
- if httpMethod != "CONNECT" && rPath != "/" {
+ if httpMethod != http.MethodConnect && rPath != "/" {
c.handlers = engine.allAutoRedirect
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
@@ -453,7 +588,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod {
continue
}
- if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
+ if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
diff --git a/gin_integration_test.go b/gin_integration_test.go
index 5f508c70..8c22e7bd 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -14,6 +14,7 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "path/filepath"
"sync"
"testing"
"time"
@@ -21,7 +22,15 @@ import (
"github.com/stretchr/testify/assert"
)
-func testRequest(t *testing.T, url string) {
+// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
+// params[1]=response status (custom compare status) default:"200 OK"
+// params[2]=response body (custom compare content) default:"it worked"
+func testRequest(t *testing.T, params ...string) {
+
+ if len(params) == 0 {
+ t.Fatal("url cannot be empty")
+ }
+
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
@@ -29,14 +38,27 @@ func testRequest(t *testing.T, url string) {
}
client := &http.Client{Transport: tr}
- resp, err := client.Get(url)
+ resp, err := client.Get(params[0])
assert.NoError(t, err)
defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body)
assert.NoError(t, ioerr)
- assert.Equal(t, "it worked", string(body), "resp body should match")
- assert.Equal(t, "200 OK", resp.Status, "should get a 200")
+
+ var responseStatus = "200 OK"
+ if len(params) > 1 && params[1] != "" {
+ responseStatus = params[1]
+ }
+
+ var responseBody = "it worked"
+ if len(params) > 2 && params[2] != "" {
+ responseBody = params[2]
+ }
+
+ assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
+ if responseStatus == "200 OK" {
+ assert.Equal(t, responseBody, string(body), "resp body should match")
+ }
}
func TestRunEmpty(t *testing.T) {
@@ -54,6 +76,81 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example")
}
+func TestBadTrustedCIDRs(t *testing.T) {
+ router := New()
+ assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
+}
+
+/* legacy tests
+func TestBadTrustedCIDRsForRun(t *testing.T) {
+ os.Setenv("PORT", "")
+ router := New()
+ router.TrustedProxies = []string{"hello/world"}
+ assert.Error(t, router.Run(":8080"))
+}
+
+func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
+ router := New()
+ router.TrustedProxies = []string{"hello/world"}
+
+ unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
+
+ defer os.Remove(unixTestSocket)
+
+ go func() {
+ router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+ assert.Error(t, router.RunUnix(unixTestSocket))
+ }()
+ // have to wait for the goroutine to start and run the server
+ // otherwise the main thread will complete
+ time.Sleep(5 * time.Millisecond)
+}
+
+func TestBadTrustedCIDRsForRunFd(t *testing.T) {
+ router := New()
+ router.TrustedProxies = []string{"hello/world"}
+
+ addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
+ assert.NoError(t, err)
+ listener, err := net.ListenTCP("tcp", addr)
+ assert.NoError(t, err)
+ socketFile, err := listener.File()
+ assert.NoError(t, err)
+
+ go func() {
+ router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+ assert.Error(t, router.RunFd(int(socketFile.Fd())))
+ }()
+ // have to wait for the goroutine to start and run the server
+ // otherwise the main thread will complete
+ time.Sleep(5 * time.Millisecond)
+}
+
+func TestBadTrustedCIDRsForRunListener(t *testing.T) {
+ router := New()
+ router.TrustedProxies = []string{"hello/world"}
+
+ addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
+ assert.NoError(t, err)
+ listener, err := net.ListenTCP("tcp", addr)
+ assert.NoError(t, err)
+ go func() {
+ router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+ assert.Error(t, router.RunListener(listener))
+ }()
+ // have to wait for the goroutine to start and run the server
+ // otherwise the main thread will complete
+ time.Sleep(5 * time.Millisecond)
+}
+
+func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
+ os.Setenv("PORT", "")
+ router := New()
+ router.TrustedProxies = []string{"hello/world"}
+ assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+}
+*/
+
func TestRunTLS(t *testing.T) {
router := New()
go func() {
@@ -146,7 +243,7 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) {
router := New()
- unixTestSocket := "/tmp/unix_unit_test"
+ unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
@@ -304,3 +401,149 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}
+
+func TestTreeRunDynamicRouting(t *testing.T) {
+ router := New()
+ router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
+ router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") })
+ router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") })
+ router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") })
+ router.GET("/c1/:dd/e", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e") })
+ router.GET("/c1/:dd/e1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e1") })
+ router.GET("/c1/:dd/f1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f1") })
+ router.GET("/c1/:dd/f2", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f2") })
+ router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") })
+ router.GET("/:cc/:dd/ee", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/ee") })
+ router.GET("/:cc/:dd/f", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/f") })
+ router.GET("/:cc/:dd/:ee/ff", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/ff") })
+ router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") })
+ router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") })
+ router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") })
+ router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") })
+ router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") })
+ router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") })
+ router.GET("/get/abc", func(c *Context) { c.String(http.StatusOK, "/get/abc") })
+ router.GET("/get/:param", func(c *Context) { c.String(http.StatusOK, "/get/:param") })
+ router.GET("/get/abc/123abc", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc") })
+ router.GET("/get/abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param") })
+ router.GET("/get/abc/123abc/xxx8", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8") })
+ router.GET("/get/abc/123abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/:param") })
+ router.GET("/get/abc/123abc/xxx8/1234", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234") })
+ router.GET("/get/abc/123abc/xxx8/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/:param") })
+ router.GET("/get/abc/123abc/xxx8/1234/ffas", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/ffas") })
+ router.GET("/get/abc/123abc/xxx8/1234/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/:param") })
+ router.GET("/get/abc/123abc/xxx8/1234/kkdd/12c", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/12c") })
+ router.GET("/get/abc/123abc/xxx8/1234/kkdd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/:param") })
+ router.GET("/get/abc/:param/test", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param/test") })
+ router.GET("/get/abc/123abd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abd/:param") })
+ router.GET("/get/abc/123abddd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abddd/:param") })
+ router.GET("/get/abc/123/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123/:param") })
+ router.GET("/get/abc/123abg/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abg/:param") })
+ router.GET("/get/abc/123abf/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abf/:param") })
+ router.GET("/get/abc/123abfff/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abfff/:param") })
+
+ ts := httptest.NewServer(router)
+ defer ts.Close()
+
+ testRequest(t, ts.URL+"/", "", "home")
+ testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx")
+ testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx")
+ testRequest(t, ts.URL+"/all", "", "/:cc")
+ testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/c1/d/e", "", "/c1/:dd/e")
+ testRequest(t, ts.URL+"/c1/d/e1", "", "/c1/:dd/e1")
+ testRequest(t, ts.URL+"/c1/d/ee", "", "/:cc/:dd/ee")
+ testRequest(t, ts.URL+"/c1/d/f", "", "/:cc/:dd/f")
+ testRequest(t, ts.URL+"/c/d/ee", "", "/:cc/:dd/ee")
+ testRequest(t, ts.URL+"/c/d/e/ff", "", "/:cc/:dd/:ee/ff")
+ testRequest(t, ts.URL+"/c/d/e/f/gg", "", "/:cc/:dd/:ee/:ff/gg")
+ testRequest(t, ts.URL+"/c/d/e/f/g/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
+ testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
+ testRequest(t, ts.URL+"/a", "", "/:cc")
+ testRequest(t, ts.URL+"/d", "", "/:cc")
+ testRequest(t, ts.URL+"/ad", "", "/:cc")
+ testRequest(t, ts.URL+"/dd", "", "/:cc")
+ testRequest(t, ts.URL+"/aa", "", "/:cc")
+ testRequest(t, ts.URL+"/aaa", "", "/:cc")
+ testRequest(t, ts.URL+"/aaa/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/ab", "", "/:cc")
+ testRequest(t, ts.URL+"/abb", "", "/:cc")
+ testRequest(t, ts.URL+"/abb/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/dddaa", "", "/:cc")
+ testRequest(t, ts.URL+"/allxxxx", "", "/:cc")
+ testRequest(t, ts.URL+"/alldd", "", "/:cc")
+ testRequest(t, ts.URL+"/cc/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/ccc/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/deedwjfs/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/acllcc/cc", "", "/:cc/cc")
+ testRequest(t, ts.URL+"/get/test/abc/", "", "/get/test/abc/")
+ testRequest(t, ts.URL+"/get/testaa/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/te/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/xx/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/tt/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/a/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/")
+ testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test")
+ testRequest(t, ts.URL+"/something/secondthingaaaa/thirdthing", "", "/something/:paramname/thirdthing")
+ testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing")
+ testRequest(t, ts.URL+"/something/se/thirdthing", "", "/something/:paramname/thirdthing")
+ testRequest(t, ts.URL+"/something/s/thirdthing", "", "/something/:paramname/thirdthing")
+ testRequest(t, ts.URL+"/something/secondthing/thirdthing", "", "/something/:paramname/thirdthing")
+ testRequest(t, ts.URL+"/get/abc", "", "/get/abc")
+ testRequest(t, ts.URL+"/get/a", "", "/get/:param")
+ testRequest(t, ts.URL+"/get/abz", "", "/get/:param")
+ testRequest(t, ts.URL+"/get/12a", "", "/get/:param")
+ testRequest(t, ts.URL+"/get/abcd", "", "/get/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc", "", "/get/abc/123abc")
+ testRequest(t, ts.URL+"/get/abc/12", "", "/get/abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123ab", "", "/get/abc/:param")
+ testRequest(t, ts.URL+"/get/abc/xyz", "", "/get/abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123abcddxx", "", "/get/abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8", "", "/get/abc/123abc/xxx8")
+ testRequest(t, ts.URL+"/get/abc/123abc/x", "", "/get/abc/123abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx", "", "/get/abc/123abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/abc", "", "/get/abc/123abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8xxas", "", "/get/abc/123abc/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234", "", "/get/abc/123abc/xxx8/1234")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1", "", "/get/abc/123abc/xxx8/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/123", "", "/get/abc/123abc/xxx8/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/78k", "", "/get/abc/123abc/xxx8/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234xxxd", "", "/get/abc/123abc/xxx8/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas", "", "/get/abc/123abc/xxx8/1234/ffas")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/f", "", "/get/abc/123abc/xxx8/1234/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffa", "", "/get/abc/123abc/xxx8/1234/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kka", "", "/get/abc/123abc/xxx8/1234/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas321", "", "/get/abc/123abc/xxx8/1234/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c", "", "/get/abc/123abc/xxx8/1234/kkdd/12c")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/1", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12b", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/34", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
+ testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
+ testRequest(t, ts.URL+"/get/abc/12/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123abdd/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123abdddf/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123ab/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123abgg/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123abff/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123abffff/test", "", "/get/abc/:param/test")
+ testRequest(t, ts.URL+"/get/abc/123abd/test", "", "/get/abc/123abd/:param")
+ testRequest(t, ts.URL+"/get/abc/123abddd/test", "", "/get/abc/123abddd/:param")
+ testRequest(t, ts.URL+"/get/abc/123/test22", "", "/get/abc/123/:param")
+ testRequest(t, ts.URL+"/get/abc/123abg/test", "", "/get/abc/123abg/:param")
+ testRequest(t, ts.URL+"/get/abc/123abf/testss", "", "/get/abc/123abf/:param")
+ testRequest(t, ts.URL+"/get/abc/123abfff/te", "", "/get/abc/123abfff/:param")
+ // 404 not found
+ testRequest(t, ts.URL+"/c/d/e", "404 Not Found")
+ testRequest(t, ts.URL+"/c/d/e1", "404 Not Found")
+ testRequest(t, ts.URL+"/c/d/eee", "404 Not Found")
+ testRequest(t, ts.URL+"/c1/d/eee", "404 Not Found")
+ testRequest(t, ts.URL+"/c1/d/e2", "404 Not Found")
+ testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh1", "404 Not Found")
+ testRequest(t, ts.URL+"/a/dd", "404 Not Found")
+ testRequest(t, ts.URL+"/addr/dd/aa", "404 Not Found")
+ testRequest(t, ts.URL+"/something/secondthing/121", "404 Not Found")
+}
diff --git a/gin_test.go b/gin_test.go
index f93a4e6b..d7e73027 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -9,6 +9,7 @@ import (
"fmt"
"html/template"
"io/ioutil"
+ "net"
"net/http"
"net/http/httptest"
"reflect"
@@ -585,6 +586,120 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter)
}
+func TestPrepareTrustedCIRDsWith(t *testing.T) {
+ r := New()
+
+ // valid ipv4 cidr
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
+ err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
+ }
+
+ // invalid ipv4 cidr
+ {
+ err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
+
+ assert.Error(t, err)
+ }
+
+ // valid ipv4 address
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
+
+ err := r.SetTrustedProxies([]string{"192.168.1.33"})
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
+ }
+
+ // invalid ipv4 address
+ {
+ err := r.SetTrustedProxies([]string{"192.168.1.256"})
+
+ assert.Error(t, err)
+ }
+
+ // valid ipv6 address
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
+ err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
+ }
+
+ // invalid ipv6 address
+ {
+ err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
+
+ assert.Error(t, err)
+ }
+
+ // valid ipv6 cidr
+ {
+ expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
+ err := r.SetTrustedProxies([]string{"::/0"})
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
+ }
+
+ // invalid ipv6 cidr
+ {
+ err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
+
+ assert.Error(t, err)
+ }
+
+ // valid combination
+ {
+ expectedTrustedCIDRs := []*net.IPNet{
+ parseCIDR("::/0"),
+ parseCIDR("192.168.0.0/16"),
+ parseCIDR("172.16.0.1/32"),
+ }
+ err := r.SetTrustedProxies([]string{
+ "::/0",
+ "192.168.0.0/16",
+ "172.16.0.1",
+ })
+
+ assert.NoError(t, err)
+ assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
+ }
+
+ // invalid combination
+ {
+ err := r.SetTrustedProxies([]string{
+ "::/0",
+ "192.168.0.0/16",
+ "172.16.0.256",
+ })
+
+ assert.Error(t, err)
+ }
+
+ // nil value
+ {
+ err := r.SetTrustedProxies(nil)
+
+ assert.Nil(t, r.trustedCIDRs)
+ assert.Nil(t, err)
+ }
+
+}
+
+func parseCIDR(cidr string) *net.IPNet {
+ _, parsedCIDR, err := net.ParseCIDR(cidr)
+ if err != nil {
+ fmt.Println(err)
+ }
+ return parsedCIDR
+}
+
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
diff --git a/go.mod b/go.mod
index cfaee746..c25eecf9 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,12 @@ go 1.13
require (
github.com/gin-contrib/sse v0.1.0
- github.com/go-playground/validator/v10 v10.2.0
- github.com/golang/protobuf v1.3.3
- github.com/json-iterator/go v1.1.9
- github.com/mattn/go-isatty v0.0.12
- github.com/stretchr/testify v1.4.0
- github.com/ugorji/go/codec v1.1.7
- gopkg.in/yaml.v2 v2.2.8
+ github.com/go-playground/validator/v10 v10.9.0
+ github.com/goccy/go-json v0.7.10
+ github.com/json-iterator/go v1.1.12
+ github.com/mattn/go-isatty v0.0.14
+ github.com/stretchr/testify v1.7.0
+ github.com/ugorji/go/codec v1.2.6
+ google.golang.org/protobuf v1.27.1
+ gopkg.in/yaml.v2 v2.4.0
)
diff --git a/go.sum b/go.sum
index 4c14fb83..51497a0b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -5,41 +6,76 @@ 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.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
-github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
-github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
-github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
-github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
-github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
+github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec=
+github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
-github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
-github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
+github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
+github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
+github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/bytesconv/bytesconv.go b/internal/bytesconv/bytesconv.go
index 7b80e335..86e4c4d4 100644
--- a/internal/bytesconv/bytesconv.go
+++ b/internal/bytesconv/bytesconv.go
@@ -5,16 +5,17 @@
package bytesconv
import (
- "reflect"
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
-func StringToBytes(s string) (b []byte) {
- sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
- bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
- bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
- return b
+func StringToBytes(s string) []byte {
+ return *(*[]byte)(unsafe.Pointer(
+ &struct {
+ string
+ Cap int
+ }{s, len(s)},
+ ))
}
// BytesToString converts byte slice to string without a memory allocation.
diff --git a/internal/json/go_json.go b/internal/json/go_json.go
new file mode 100644
index 00000000..da960571
--- /dev/null
+++ b/internal/json/go_json.go
@@ -0,0 +1,23 @@
+// Copyright 2017 Bo-Yi Wu. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+//go:build go_json
+// +build go_json
+
+package json
+
+import json "github.com/goccy/go-json"
+
+var (
+ // Marshal is exported by gin/json package.
+ Marshal = json.Marshal
+ // Unmarshal is exported by gin/json package.
+ Unmarshal = json.Unmarshal
+ // MarshalIndent is exported by gin/json package.
+ MarshalIndent = json.MarshalIndent
+ // NewDecoder is exported by gin/json package.
+ NewDecoder = json.NewDecoder
+ // NewEncoder is exported by gin/json package.
+ NewEncoder = json.NewEncoder
+)
diff --git a/internal/json/json.go b/internal/json/json.go
index 480e8bff..75b60224 100644
--- a/internal/json/json.go
+++ b/internal/json/json.go
@@ -2,7 +2,8 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
-// +build !jsoniter
+//go:build !jsoniter && !go_json
+// +build !jsoniter,!go_json
package json
diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go
index 649a3cdb..232f8dca 100644
--- a/internal/json/jsoniter.go
+++ b/internal/json/jsoniter.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build jsoniter
// +build jsoniter
package json
diff --git a/logger.go b/logger.go
index d361b74d..22138a8d 100644
--- a/logger.go
+++ b/logger.go
@@ -138,8 +138,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
}
if param.Latency > time.Minute {
- // Truncate in a golang < 1.8 safe way
- param.Latency = param.Latency - param.Latency%time.Second
+ param.Latency = param.Latency.Truncate(time.Second)
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
diff --git a/logger_test.go b/logger_test.go
index 0d40666e..80961ce1 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -185,6 +185,8 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
+ router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
+
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Formatter: func(param LogFormatterParams) string {
diff --git a/middleware_test.go b/middleware_test.go
index fca1c530..4b4afd4a 100644
--- a/middleware_test.go
+++ b/middleware_test.go
@@ -118,7 +118,10 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
func TestMiddlewareNoMethodDisabled(t *testing.T) {
signature := ""
router := New()
+
+ // NoMethod disabled
router.HandleMethodNotAllowed = false
+
router.Use(func(c *Context) {
signature += "A"
c.Next()
@@ -144,6 +147,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
router.POST("/", func(c *Context) {
signature += " XX "
})
+
// RUN
w := performRequest(router, "GET", "/")
diff --git a/mode.go b/mode.go
index 11f833e9..4d199df3 100644
--- a/mode.go
+++ b/mode.go
@@ -41,8 +41,10 @@ var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr
-var ginMode = debugCode
-var modeName = DebugMode
+var (
+ ginMode = debugCode
+ modeName = DebugMode
+)
func init() {
mode := os.Getenv(EnvGinMode)
@@ -63,7 +65,7 @@ func SetMode(value string) {
case TestMode:
ginMode = testCode
default:
- panic("gin mode unknown: " + value)
+ panic("gin mode unknown: " + value + " (available mode: debug release test)")
}
modeName = value
diff --git a/recovery.go b/recovery.go
index d02b829b..39f13551 100644
--- a/recovery.go
+++ b/recovery.go
@@ -6,6 +6,7 @@ package gin
import (
"bytes"
+ "errors"
"fmt"
"io"
"io/ioutil"
@@ -34,7 +35,7 @@ func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
-//CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
+// CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter, handle)
}
@@ -60,7 +61,8 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
- if se, ok := ne.Err.(*os.SyscallError); ok {
+ var se *os.SyscallError
+ if errors.As(ne, &se) {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
@@ -76,11 +78,12 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
headers[idx] = current[0] + ": *"
}
}
+ headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
- logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
+ logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
- timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
+ timeFormat(time.Now()), headersToStr, err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
@@ -164,7 +167,7 @@ func function(pc uintptr) []byte {
return name
}
+// timeFormat returns a customized time string for logger.
func timeFormat(t time.Time) string {
- timeString := t.Format("2006/01/02 - 15:04:05")
- return timeString
+ return t.Format("2006/01/02 - 15:04:05")
}
diff --git a/recovery_test.go b/recovery_test.go
index 6cc2a47a..d164bfa3 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -92,14 +92,14 @@ func TestPanicWithAbort(t *testing.T) {
func TestSource(t *testing.T) {
bs := source(nil, 0)
- assert.Equal(t, []byte("???"), bs)
+ assert.Equal(t, dunno, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
- assert.Equal(t, []byte("???"), bs)
+ assert.Equal(t, dunno, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
@@ -107,7 +107,7 @@ func TestSource(t *testing.T) {
func TestFunction(t *testing.T) {
bs := function(1)
- assert.Equal(t, []byte("???"), bs)
+ assert.Equal(t, dunno, bs)
}
// TestPanicWithBrokenPipe asserts that recovery specifically handles
diff --git a/render/json.go b/render/json.go
index 41863093..3ebcee97 100644
--- a/render/json.go
+++ b/render/json.go
@@ -46,9 +46,11 @@ type PureJSON struct {
Data interface{}
}
-var jsonContentType = []string{"application/json; charset=utf-8"}
-var jsonpContentType = []string{"application/javascript; charset=utf-8"}
-var jsonAsciiContentType = []string{"application/json"}
+var (
+ jsonContentType = []string{"application/json; charset=utf-8"}
+ jsonpContentType = []string{"application/javascript; charset=utf-8"}
+ jsonASCIIContentType = []string{"application/json"}
+)
// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
@@ -100,8 +102,7 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
// if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
bytesconv.StringToBytes("]")) {
- _, err = w.Write(bytesconv.StringToBytes(r.Prefix))
- if err != nil {
+ if _, err = w.Write(bytesconv.StringToBytes(r.Prefix)); err != nil {
return err
}
}
@@ -128,20 +129,19 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
}
callback := template.JSEscapeString(r.Callback)
- _, err = w.Write(bytesconv.StringToBytes(callback))
- if err != nil {
+ if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil {
return err
}
- _, err = w.Write(bytesconv.StringToBytes("("))
- if err != nil {
+
+ if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
return err
}
- _, err = w.Write(ret)
- if err != nil {
+
+ if _, err = w.Write(ret); err != nil {
return err
}
- _, err = w.Write(bytesconv.StringToBytes(");"))
- if err != nil {
+
+ if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
return err
}
@@ -176,7 +176,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
// WriteContentType (AsciiJSON) writes JSON ContentType.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
- writeContentType(w, jsonAsciiContentType)
+ writeContentType(w, jsonASCIIContentType)
}
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
diff --git a/render/msgpack.go b/render/msgpack.go
index be2d45c5..7f17ca4d 100644
--- a/render/msgpack.go
+++ b/render/msgpack.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
// +build !nomsgpack
package render
@@ -12,6 +13,8 @@ import (
"github.com/ugorji/go/codec"
)
+// Check interface implemented here to support go build tag nomsgpack.
+// See: https://github.com/gin-gonic/gin/pull/1852/
var (
_ Render = MsgPack{}
)
diff --git a/render/protobuf.go b/render/protobuf.go
index 15aca995..1d2aa871 100644
--- a/render/protobuf.go
+++ b/render/protobuf.go
@@ -7,7 +7,7 @@ package render
import (
"net/http"
- "github.com/golang/protobuf/proto"
+ "google.golang.org/protobuf/proto"
)
// ProtoBuf contains the given interface object.
diff --git a/render/render_msgpack_test.go b/render/render_msgpack_test.go
index e439ac48..d16cf6e6 100644
--- a/render/render_msgpack_test.go
+++ b/render/render_msgpack_test.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
+//go:build !nomsgpack
// +build !nomsgpack
package render
@@ -38,6 +39,6 @@ func TestRenderMsgPack(t *testing.T) {
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
- assert.Equal(t, w.Body.String(), string(buf.Bytes()))
+ assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
diff --git a/render/render_test.go b/render/render_test.go
index 353c82bb..e417731a 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -14,10 +14,9 @@ import (
"strings"
"testing"
- "github.com/golang/protobuf/proto"
- "github.com/stretchr/testify/assert"
-
testdata "github.com/gin-gonic/gin/testdata/protoexample"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/proto"
)
// TODO unit tests
@@ -420,7 +419,8 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
- htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
+ htmlRender := HTMLDebug{
+ Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
@@ -438,7 +438,8 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
- htmlRender := HTMLDebug{Files: nil,
+ htmlRender := HTMLDebug{
+ Files: nil,
Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
@@ -455,7 +456,8 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
}
func TestRenderHTMLDebugPanics(t *testing.T) {
- htmlRender := HTMLDebug{Files: nil,
+ htmlRender := HTMLDebug{
+ Files: nil,
Glob: "",
Delims: Delims{"{{", "}}"},
FuncMap: nil,
diff --git a/render/text.go b/render/text.go
index 30f5f532..461b720a 100644
--- a/render/text.go
+++ b/render/text.go
@@ -7,6 +7,8 @@ package render
import (
"fmt"
"net/http"
+
+ "github.com/gin-gonic/gin/internal/bytesconv"
)
// String contains the given interface object slice and its format.
@@ -34,6 +36,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err
_, err = fmt.Fprintf(w, format, data...)
return
}
- _, err = w.Write([]byte(format))
+ _, err = w.Write(bytesconv.StringToBytes(format))
return
}
diff --git a/response_writer_test.go b/response_writer_test.go
index 1f113e74..9061d021 100644
--- a/response_writer_test.go
+++ b/response_writer_test.go
@@ -17,12 +17,14 @@ import (
// func (w *responseWriter) CloseNotify() <-chan bool {
// func (w *responseWriter) Flush() {
-var _ ResponseWriter = &responseWriter{}
-var _ http.ResponseWriter = &responseWriter{}
-var _ http.ResponseWriter = ResponseWriter(&responseWriter{})
-var _ http.Hijacker = ResponseWriter(&responseWriter{})
-var _ http.Flusher = ResponseWriter(&responseWriter{})
-var _ http.CloseNotifier = ResponseWriter(&responseWriter{})
+var (
+ _ ResponseWriter = &responseWriter{}
+ _ http.ResponseWriter = &responseWriter{}
+ _ http.ResponseWriter = ResponseWriter(&responseWriter{})
+ _ http.Hijacker = ResponseWriter(&responseWriter{})
+ _ http.Flusher = ResponseWriter(&responseWriter{})
+ _ http.CloseNotifier = ResponseWriter(&responseWriter{})
+)
func init() {
SetMode(TestMode)
diff --git a/routergroup.go b/routergroup.go
index 15d9930d..27d7aad6 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -11,6 +11,18 @@ import (
"strings"
)
+var (
+ // reg match english letters for http method name
+ regEnLetter = regexp.MustCompile("^[A-Z]+$")
+
+ // anyMethods for RouterGroup Any method
+ anyMethods = []string{
+ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
+ http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
+ http.MethodTrace,
+ }
+)
+
// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
IRoutes
@@ -87,7 +99,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
- if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
+ if matched := regEnLetter.MatchString(httpMethod); !matched {
panic("http method " + httpMethod + " is not valid")
}
return group.handle(httpMethod, relativePath, handlers)
@@ -131,15 +143,10 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRo
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
- group.handle(http.MethodGet, relativePath, handlers)
- group.handle(http.MethodPost, relativePath, handlers)
- group.handle(http.MethodPut, relativePath, handlers)
- group.handle(http.MethodPatch, relativePath, handlers)
- group.handle(http.MethodHead, relativePath, handlers)
- group.handle(http.MethodOptions, relativePath, handlers)
- group.handle(http.MethodDelete, relativePath, handlers)
- group.handle(http.MethodConnect, relativePath, handlers)
- group.handle(http.MethodTrace, relativePath, handlers)
+ for _, method := range anyMethods {
+ group.handle(method, relativePath, handlers)
+ }
+
return group.returnObj()
}
@@ -209,9 +216,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
- if finalSize >= int(abortIndex) {
- panic("too many handlers")
- }
+ assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
diff --git a/routergroup_test.go b/routergroup_test.go
index 0e49d65b..d6d8b452 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -112,15 +112,19 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
}
func TestRouterGroupTooManyHandlers(t *testing.T) {
+ const (
+ panicValue = "too many handlers"
+ maximumCnt = abortIndex
+ )
router := New()
- handlers1 := make([]HandlerFunc, 40)
+ handlers1 := make([]HandlerFunc, maximumCnt-1)
router.Use(handlers1...)
- handlers2 := make([]HandlerFunc, 26)
- assert.Panics(t, func() {
+ handlers2 := make([]HandlerFunc, maximumCnt+1)
+ assert.PanicsWithValue(t, panicValue, func() {
router.Use(handlers2...)
})
- assert.Panics(t, func() {
+ assert.PanicsWithValue(t, panicValue, func() {
router.GET("/", handlers2...)
})
}
diff --git a/routes_test.go b/routes_test.go
index 8c919b03..b96978e3 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -259,7 +259,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
- assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@@ -293,7 +292,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
- assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@@ -383,7 +381,9 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin")
- assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
+ // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
+ // else, Content-Type='text/x-go; charset=utf-8'
+ assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
@@ -502,6 +502,21 @@ func TestRouterNotFound(t *testing.T) {
router.GET("/a", func(c *Context) {})
w = performRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code)
+
+ // Reproduction test for the bug of issue #2843
+ router = New()
+ router.NoRoute(func(c *Context) {
+ if c.Request.RequestURI == "/login" {
+ c.String(200, "login")
+ }
+ })
+ router.GET("/logout", func(c *Context) {
+ c.String(200, "logout")
+ })
+ w = performRequest(router, http.MethodGet, "/login")
+ assert.Equal(t, "login", w.Body.String())
+ w = performRequest(router, http.MethodGet, "/logout")
+ assert.Equal(t, "logout", w.Body.String())
}
func TestRouterStaticFSNotFound(t *testing.T) {
diff --git a/testdata/protoexample/test.pb.go b/testdata/protoexample/test.pb.go
index 21997ca1..bf45e028 100644
--- a/testdata/protoexample/test.pb.go
+++ b/testdata/protoexample/test.pb.go
@@ -1,24 +1,24 @@
-// Code generated by protoc-gen-go.
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.27.0
+// protoc v3.15.8
// source: test.proto
-// DO NOT EDIT!
-/*
-Package protoexample is a generated protocol buffer package.
-
-It is generated from these files:
- test.proto
-
-It has these top-level messages:
- Test
-*/
package protoexample
-import proto "github.com/golang/protobuf/proto"
-import math "math"
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
-// Reference imports to suppress errors if they are not otherwise used.
-var _ = proto.Marshal
-var _ = math.Inf
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
type FOO int32
@@ -26,88 +26,273 @@ const (
FOO_X FOO = 17
)
-var FOO_name = map[int32]string{
- 17: "X",
-}
-var FOO_value = map[string]int32{
- "X": 17,
-}
+// Enum value maps for FOO.
+var (
+ FOO_name = map[int32]string{
+ 17: "X",
+ }
+ FOO_value = map[string]int32{
+ "X": 17,
+ }
+)
func (x FOO) Enum() *FOO {
p := new(FOO)
*p = x
return p
}
+
func (x FOO) String() string {
- return proto.EnumName(FOO_name, int32(x))
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
-func (x *FOO) UnmarshalJSON(data []byte) error {
- value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO")
+
+func (FOO) Descriptor() protoreflect.EnumDescriptor {
+ return file_test_proto_enumTypes[0].Descriptor()
+}
+
+func (FOO) Type() protoreflect.EnumType {
+ return &file_test_proto_enumTypes[0]
+}
+
+func (x FOO) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Do not use.
+func (x *FOO) UnmarshalJSON(b []byte) error {
+ num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
if err != nil {
return err
}
- *x = FOO(value)
+ *x = FOO(num)
return nil
}
-type Test struct {
- Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
- Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
- Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
- Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
- XXX_unrecognized []byte `json:"-"`
+// Deprecated: Use FOO.Descriptor instead.
+func (FOO) EnumDescriptor() ([]byte, []int) {
+ return file_test_proto_rawDescGZIP(), []int{0}
}
-func (m *Test) Reset() { *m = Test{} }
-func (m *Test) String() string { return proto.CompactTextString(m) }
-func (*Test) ProtoMessage() {}
+type Test struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
-const Default_Test_Type int32 = 77
+ Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
+ Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
+ Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
+ Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup,json=optionalgroup" json:"optionalgroup,omitempty"`
+}
-func (m *Test) GetLabel() string {
- if m != nil && m.Label != nil {
- return *m.Label
+// Default values for Test fields.
+const (
+ Default_Test_Type = int32(77)
+)
+
+func (x *Test) Reset() {
+ *x = Test{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_test_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Test) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Test) ProtoMessage() {}
+
+func (x *Test) ProtoReflect() protoreflect.Message {
+ mi := &file_test_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Test.ProtoReflect.Descriptor instead.
+func (*Test) Descriptor() ([]byte, []int) {
+ return file_test_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Test) GetLabel() string {
+ if x != nil && x.Label != nil {
+ return *x.Label
}
return ""
}
-func (m *Test) GetType() int32 {
- if m != nil && m.Type != nil {
- return *m.Type
+func (x *Test) GetType() int32 {
+ if x != nil && x.Type != nil {
+ return *x.Type
}
return Default_Test_Type
}
-func (m *Test) GetReps() []int64 {
- if m != nil {
- return m.Reps
+func (x *Test) GetReps() []int64 {
+ if x != nil {
+ return x.Reps
}
return nil
}
-func (m *Test) GetOptionalgroup() *Test_OptionalGroup {
- if m != nil {
- return m.Optionalgroup
+func (x *Test) GetOptionalgroup() *Test_OptionalGroup {
+ if x != nil {
+ return x.Optionalgroup
}
return nil
}
type Test_OptionalGroup struct {
- RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"`
- XXX_unrecognized []byte `json:"-"`
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ RequiredField *string `protobuf:"bytes,5,req,name=RequiredField" json:"RequiredField,omitempty"`
}
-func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} }
-func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) }
-func (*Test_OptionalGroup) ProtoMessage() {}
+func (x *Test_OptionalGroup) Reset() {
+ *x = Test_OptionalGroup{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_test_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
-func (m *Test_OptionalGroup) GetRequiredField() string {
- if m != nil && m.RequiredField != nil {
- return *m.RequiredField
+func (x *Test_OptionalGroup) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Test_OptionalGroup) ProtoMessage() {}
+
+func (x *Test_OptionalGroup) ProtoReflect() protoreflect.Message {
+ mi := &file_test_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Test_OptionalGroup.ProtoReflect.Descriptor instead.
+func (*Test_OptionalGroup) Descriptor() ([]byte, []int) {
+ return file_test_proto_rawDescGZIP(), []int{0, 0}
+}
+
+func (x *Test_OptionalGroup) GetRequiredField() string {
+ if x != nil && x.RequiredField != nil {
+ return *x.RequiredField
}
return ""
}
-func init() {
- proto.RegisterEnum("protoexample.FOO", FOO_name, FOO_value)
+var File_test_proto protoreflect.FileDescriptor
+
+var file_test_proto_rawDesc = []byte{
+ 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x04, 0x54,
+ 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x02,
+ 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x74, 0x79, 0x70,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x3a, 0x02, 0x37, 0x37, 0x52, 0x04, 0x74, 0x79, 0x70,
+ 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52,
+ 0x04, 0x72, 0x65, 0x70, 0x73, 0x12, 0x46, 0x0a, 0x0d, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61,
+ 0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0a, 0x32, 0x20, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x54, 0x65, 0x73, 0x74,
+ 0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d,
+ 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x1a, 0x35, 0x0a,
+ 0x0d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x24,
+ 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18,
+ 0x05, 0x20, 0x02, 0x28, 0x09, 0x52, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46,
+ 0x69, 0x65, 0x6c, 0x64, 0x2a, 0x0c, 0x0a, 0x03, 0x46, 0x4f, 0x4f, 0x12, 0x05, 0x0a, 0x01, 0x58,
+ 0x10, 0x11,
+}
+
+var (
+ file_test_proto_rawDescOnce sync.Once
+ file_test_proto_rawDescData = file_test_proto_rawDesc
+)
+
+func file_test_proto_rawDescGZIP() []byte {
+ file_test_proto_rawDescOnce.Do(func() {
+ file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData)
+ })
+ return file_test_proto_rawDescData
+}
+
+var file_test_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_test_proto_goTypes = []interface{}{
+ (FOO)(0), // 0: protoexample.FOO
+ (*Test)(nil), // 1: protoexample.Test
+ (*Test_OptionalGroup)(nil), // 2: protoexample.Test.OptionalGroup
+}
+var file_test_proto_depIdxs = []int32{
+ 2, // 0: protoexample.Test.optionalgroup:type_name -> protoexample.Test.OptionalGroup
+ 1, // [1:1] is the sub-list for method output_type
+ 1, // [1:1] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_test_proto_init() }
+func file_test_proto_init() {
+ if File_test_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Test); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Test_OptionalGroup); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_test_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_test_proto_goTypes,
+ DependencyIndexes: file_test_proto_depIdxs,
+ EnumInfos: file_test_proto_enumTypes,
+ MessageInfos: file_test_proto_msgTypes,
+ }.Build()
+ File_test_proto = out.File
+ file_test_proto_rawDesc = nil
+ file_test_proto_goTypes = nil
+ file_test_proto_depIdxs = nil
}
diff --git a/tree.go b/tree.go
index 7a80af9e..2f0de1a9 100644
--- a/tree.go
+++ b/tree.go
@@ -17,6 +17,7 @@ import (
var (
strColon = []byte(":")
strStar = []byte("*")
+ strSlash = []byte("/")
)
// Param is a single URL parameter, consisting of a key and a value.
@@ -30,8 +31,8 @@ type Param struct {
// It is therefore safe to read values by the index.
type Params []Param
-// Get returns the value of the first Param which key matches the given name.
-// If no matching Param is found, an empty string is returned.
+// Get returns the value of the first Param which key matches the given name and a boolean true.
+// If no matching Param is found, an empty string is returned and a boolean false .
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
@@ -80,6 +81,16 @@ func longestCommonPrefix(a, b string) int {
return i
}
+// addChild will add a child node, keeping wildcards at the end
+func (n *node) addChild(child *node) {
+ if n.wildChild && len(n.children) > 0 {
+ wildcardChild := n.children[len(n.children)-1]
+ n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
+ } else {
+ n.children = append(n.children, child)
+ }
+}
+
func countParams(path string) uint16 {
var n uint16
s := bytesconv.StringToBytes(path)
@@ -88,11 +99,15 @@ func countParams(path string) uint16 {
return n
}
+func countSections(path string) uint16 {
+ s := bytesconv.StringToBytes(path)
+ return uint16(bytes.Count(s, strSlash))
+}
+
type nodeType uint8
const (
- static nodeType = iota // default
- root
+ root nodeType = iota + 1
param
catchAll
)
@@ -103,7 +118,7 @@ type node struct {
wildChild bool
nType nodeType
priority uint32
- children []*node
+ children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}
@@ -119,7 +134,6 @@ func (n *node) incrementChildPrio(pos int) int {
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
-
}
// Build new index char string
@@ -178,36 +192,9 @@ walk:
// Make new node a child of this node
if i < len(path) {
path = path[i:]
-
- if n.wildChild {
- parentFullPathIndex += len(n.path)
- n = n.children[0]
- n.priority++
-
- // Check if the wildcard matches
- if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
- // Adding a child to a catchAll is not possible
- n.nType != catchAll &&
- // Check for longer wildcard, e.g. :name and :names
- (len(n.path) >= len(path) || path[len(n.path)] == '/') {
- continue walk
- }
-
- pathSeg := path
- if n.nType != catchAll {
- pathSeg = strings.SplitN(path, "/", 2)[0]
- }
- prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
- panic("'" + pathSeg +
- "' in new path '" + fullPath +
- "' conflicts with existing wildcard '" + n.path +
- "' in existing prefix '" + prefix +
- "'")
- }
-
c := path[0]
- // slash after param
+ // '/' after param
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
@@ -226,21 +213,47 @@ walk:
}
// Otherwise insert it
- if c != ':' && c != '*' {
+ if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
- n.children = append(n.children, child)
+ n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
+ } else if n.wildChild {
+ // inserting a wildcard node, need to check if it conflicts with the existing wildcard
+ n = n.children[len(n.children)-1]
+ n.priority++
+
+ // Check if the wildcard matches
+ if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
+ // Adding a child to a catchAll is not possible
+ n.nType != catchAll &&
+ // Check for longer wildcard, e.g. :name and :names
+ (len(n.path) >= len(path) || path[len(n.path)] == '/') {
+ continue walk
+ }
+
+ // Wildcard conflict
+ pathSeg := path
+ if n.nType != catchAll {
+ pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
+ }
+ prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
+ panic("'" + pathSeg +
+ "' in new path '" + fullPath +
+ "' conflicts with existing wildcard '" + n.path +
+ "' in existing prefix '" + prefix +
+ "'")
}
+
n.insertChild(path, fullPath, handlers)
return
}
- // Otherwise and handle to current node
+ // Otherwise add handle to current node
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
@@ -294,13 +307,6 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
- // Check if this node has existing children which would be
- // unreachable if we insert the wildcard here
- if len(n.children) > 0 {
- panic("wildcard segment '" + wildcard +
- "' conflicts with existing children in path '" + fullPath + "'")
- }
-
if wildcard[0] == ':' { // param
if i > 0 {
// Insert prefix before the current wildcard
@@ -308,13 +314,13 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
path = path[i:]
}
- n.wildChild = true
child := &node{
nType: param,
path: wildcard,
fullPath: fullPath,
}
- n.children = []*node{child}
+ n.addChild(child)
+ n.wildChild = true
n = child
n.priority++
@@ -327,7 +333,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
priority: 1,
fullPath: fullPath,
}
- n.children = []*node{child}
+ n.addChild(child)
n = child
continue
}
@@ -361,7 +367,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
fullPath: fullPath,
}
- n.children = []*node{child}
+ n.addChild(child)
n.indices = string('/')
n = child
n.priority++
@@ -393,41 +399,90 @@ type nodeValue struct {
fullPath string
}
+type skippedNode struct {
+ path string
+ node *node
+ paramsCount int16
+}
+
// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
-func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
+func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
+ var globalParamsCount int16
+
walk: // Outer loop for walking the tree
for {
prefix := n.path
if len(path) > len(prefix) {
if path[:len(prefix)] == prefix {
path = path[len(prefix):]
- // If this node does not have a wildcard (param or catchAll)
- // child, we can just look up the next child node and continue
- // to walk down the tree
+
+ // Try all the non-wildcard children first by matching the indices
+ idxc := path[0]
+ for i, c := range []byte(n.indices) {
+ if c == idxc {
+ // strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
+ if n.wildChild {
+ index := len(*skippedNodes)
+ *skippedNodes = (*skippedNodes)[:index+1]
+ (*skippedNodes)[index] = skippedNode{
+ path: prefix + path,
+ node: &node{
+ path: n.path,
+ wildChild: n.wildChild,
+ nType: n.nType,
+ priority: n.priority,
+ children: n.children,
+ handlers: n.handlers,
+ fullPath: n.fullPath,
+ },
+ paramsCount: globalParamsCount,
+ }
+ }
+
+ n = n.children[i]
+ continue walk
+ }
+ }
+
if !n.wildChild {
- idxc := path[0]
- for i, c := range []byte(n.indices) {
- if c == idxc {
- n = n.children[i]
- continue walk
+ // If the path at the end of the loop is not equal to '/' and the current node has no child nodes
+ // the current node needs to roll back to last vaild skippedNode
+ if path != "/" {
+ for l := len(*skippedNodes); l > 0; {
+ skippedNode := (*skippedNodes)[l-1]
+ *skippedNodes = (*skippedNodes)[:l-1]
+ if strings.HasSuffix(skippedNode.path, path) {
+ path = skippedNode.path
+ n = skippedNode.node
+ if value.params != nil {
+ *value.params = (*value.params)[:skippedNode.paramsCount]
+ }
+ globalParamsCount = skippedNode.paramsCount
+ continue walk
+ }
}
}
// Nothing found.
// 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)
+ value.tsr = path == "/" && n.handlers != nil
return
}
- // Handle wildcard child
- n = n.children[0]
+ // Handle wildcard child, which is always at the end of the array
+ n = n.children[len(n.children)-1]
+ globalParamsCount++
+
switch n.nType {
case param:
+ // fix truncate the parameter
+ // tree_test.go line: 204
+
// Find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
@@ -435,7 +490,7 @@ walk: // Outer loop for walking the tree
}
// Save param value
- if params != nil {
+ if params != nil && cap(*params) > 0 {
if value.params == nil {
value.params = params
}
@@ -463,7 +518,7 @@ walk: // Outer loop for walking the tree
}
// ... but we can't
- value.tsr = (len(path) == end+1)
+ value.tsr = len(path) == end+1
return
}
@@ -475,7 +530,7 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
- value.tsr = (n.path == "/" && n.handlers != nil)
+ value.tsr = n.path == "/" && n.handlers != nil
}
return
@@ -511,6 +566,24 @@ walk: // Outer loop for walking the tree
}
if path == prefix {
+ // If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
+ // the current node needs to roll back to last vaild skippedNode
+ if n.handlers == nil && path != "/" {
+ for l := len(*skippedNodes); l > 0; {
+ skippedNode := (*skippedNodes)[l-1]
+ *skippedNodes = (*skippedNodes)[:l-1]
+ if strings.HasSuffix(skippedNode.path, path) {
+ path = skippedNode.path
+ n = skippedNode.node
+ if value.params != nil {
+ *value.params = (*value.params)[:skippedNode.paramsCount]
+ }
+ globalParamsCount = skippedNode.paramsCount
+ continue walk
+ }
+ }
+ // n = latestNode.children[len(latestNode.children)-1]
+ }
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil {
@@ -542,9 +615,27 @@ walk: // Outer loop for walking the tree
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
- value.tsr = (path == "/") ||
+ value.tsr = path == "/" ||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
path == prefix[:len(prefix)-1] && n.handlers != nil)
+
+ // roll back to last valid skippedNode
+ if !value.tsr && path != "/" {
+ for l := len(*skippedNodes); l > 0; {
+ skippedNode := (*skippedNodes)[l-1]
+ *skippedNodes = (*skippedNodes)[:l-1]
+ if strings.HasSuffix(skippedNode.path, path) {
+ path = skippedNode.path
+ n = skippedNode.node
+ if value.params != nil {
+ *value.params = (*value.params)[:skippedNode.paramsCount]
+ }
+ globalParamsCount = skippedNode.paramsCount
+ continue walk
+ }
+ }
+ }
+
return
}
}
@@ -559,8 +650,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by
// Use a static sized buffer on the stack in the common case.
// If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize)
- if l := len(path) + 1; l > stackBufSize {
- buf = make([]byte, 0, l)
+ if length := len(path) + 1; length > stackBufSize {
+ buf = make([]byte, 0, length)
}
ciPath := n.findCaseInsensitivePathRec(
@@ -600,142 +691,7 @@ walk: // Outer loop for walking the tree
path = path[npLen:]
ciPath = append(ciPath, n.path...)
- if len(path) > 0 {
- // If this node does not have a wildcard (param or catchAll) child,
- // we can just look up the next child node and continue to walk down
- // the tree
- if !n.wildChild {
- // Skip rune bytes already processed
- rb = shiftNRuneBytes(rb, npLen)
-
- if rb[0] != 0 {
- // Old rune not finished
- idxc := rb[0]
- for i, c := range []byte(n.indices) {
- if c == idxc {
- // continue with child node
- n = n.children[i]
- npLen = len(n.path)
- continue walk
- }
- }
- } else {
- // Process a new rune
- var rv rune
-
- // Find rune start.
- // Runes are up to 4 byte long,
- // -4 would definitely be another rune.
- var off int
- for max := min(npLen, 3); off < max; off++ {
- if i := npLen - off; utf8.RuneStart(oldPath[i]) {
- // read rune from cached path
- rv, _ = utf8.DecodeRuneInString(oldPath[i:])
- break
- }
- }
-
- // Calculate lowercase bytes of current rune
- lo := unicode.ToLower(rv)
- utf8.EncodeRune(rb[:], lo)
-
- // Skip already processed bytes
- rb = shiftNRuneBytes(rb, off)
-
- idxc := rb[0]
- for i, c := range []byte(n.indices) {
- // Lowercase matches
- if c == idxc {
- // must use a recursive approach since both the
- // uppercase byte and the lowercase byte might exist
- // as an index
- if out := n.children[i].findCaseInsensitivePathRec(
- path, ciPath, rb, fixTrailingSlash,
- ); out != nil {
- return out
- }
- break
- }
- }
-
- // If we found no match, the same for the uppercase rune,
- // if it differs
- if up := unicode.ToUpper(rv); up != lo {
- utf8.EncodeRune(rb[:], up)
- rb = shiftNRuneBytes(rb, off)
-
- idxc := rb[0]
- for i, c := range []byte(n.indices) {
- // Uppercase matches
- if c == idxc {
- // Continue with child node
- n = n.children[i]
- npLen = len(n.path)
- continue walk
- }
- }
- }
- }
-
- // Nothing found. We can recommend to redirect to the same URL
- // without a trailing slash if a leaf exists for that path
- if fixTrailingSlash && path == "/" && n.handlers != nil {
- return ciPath
- }
- return nil
- }
-
- n = n.children[0]
- switch n.nType {
- case param:
- // Find param end (either '/' or path end)
- end := 0
- for end < len(path) && path[end] != '/' {
- end++
- }
-
- // Add param value to case insensitive path
- ciPath = append(ciPath, path[:end]...)
-
- // We need to go deeper!
- if end < len(path) {
- if len(n.children) > 0 {
- // Continue with child node
- n = n.children[0]
- npLen = len(n.path)
- path = path[end:]
- continue
- }
-
- // ... but we can't
- if fixTrailingSlash && len(path) == end+1 {
- return ciPath
- }
- return nil
- }
-
- if n.handlers != nil {
- return ciPath
- }
-
- if fixTrailingSlash && len(n.children) == 1 {
- // No handle found. Check if a handle for this path + a
- // trailing slash exists
- n = n.children[0]
- if n.path == "/" && n.handlers != nil {
- return append(ciPath, '/')
- }
- }
-
- return nil
-
- case catchAll:
- return append(ciPath, path...)
-
- default:
- panic("invalid node type")
- }
- } else {
+ if len(path) == 0 {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handlers != nil {
@@ -758,6 +714,141 @@ walk: // Outer loop for walking the tree
}
return nil
}
+
+ // If this node does not have a wildcard (param or catchAll) child,
+ // we can just look up the next child node and continue to walk down
+ // the tree
+ if !n.wildChild {
+ // Skip rune bytes already processed
+ rb = shiftNRuneBytes(rb, npLen)
+
+ if rb[0] != 0 {
+ // Old rune not finished
+ idxc := rb[0]
+ for i, c := range []byte(n.indices) {
+ if c == idxc {
+ // continue with child node
+ n = n.children[i]
+ npLen = len(n.path)
+ continue walk
+ }
+ }
+ } else {
+ // Process a new rune
+ var rv rune
+
+ // Find rune start.
+ // Runes are up to 4 byte long,
+ // -4 would definitely be another rune.
+ var off int
+ for max := min(npLen, 3); off < max; off++ {
+ if i := npLen - off; utf8.RuneStart(oldPath[i]) {
+ // read rune from cached path
+ rv, _ = utf8.DecodeRuneInString(oldPath[i:])
+ break
+ }
+ }
+
+ // Calculate lowercase bytes of current rune
+ lo := unicode.ToLower(rv)
+ utf8.EncodeRune(rb[:], lo)
+
+ // Skip already processed bytes
+ rb = shiftNRuneBytes(rb, off)
+
+ idxc := rb[0]
+ for i, c := range []byte(n.indices) {
+ // Lowercase matches
+ if c == idxc {
+ // must use a recursive approach since both the
+ // uppercase byte and the lowercase byte might exist
+ // as an index
+ if out := n.children[i].findCaseInsensitivePathRec(
+ path, ciPath, rb, fixTrailingSlash,
+ ); out != nil {
+ return out
+ }
+ break
+ }
+ }
+
+ // If we found no match, the same for the uppercase rune,
+ // if it differs
+ if up := unicode.ToUpper(rv); up != lo {
+ utf8.EncodeRune(rb[:], up)
+ rb = shiftNRuneBytes(rb, off)
+
+ idxc := rb[0]
+ for i, c := range []byte(n.indices) {
+ // Uppercase matches
+ if c == idxc {
+ // Continue with child node
+ n = n.children[i]
+ npLen = len(n.path)
+ continue walk
+ }
+ }
+ }
+ }
+
+ // Nothing found. We can recommend to redirect to the same URL
+ // without a trailing slash if a leaf exists for that path
+ if fixTrailingSlash && path == "/" && n.handlers != nil {
+ return ciPath
+ }
+ return nil
+ }
+
+ n = n.children[0]
+ switch n.nType {
+ case param:
+ // Find param end (either '/' or path end)
+ end := 0
+ for end < len(path) && path[end] != '/' {
+ end++
+ }
+
+ // Add param value to case insensitive path
+ ciPath = append(ciPath, path[:end]...)
+
+ // We need to go deeper!
+ if end < len(path) {
+ if len(n.children) > 0 {
+ // Continue with child node
+ n = n.children[0]
+ npLen = len(n.path)
+ path = path[end:]
+ continue
+ }
+
+ // ... but we can't
+ if fixTrailingSlash && len(path) == end+1 {
+ return ciPath
+ }
+ return nil
+ }
+
+ if n.handlers != nil {
+ return ciPath
+ }
+
+ if fixTrailingSlash && len(n.children) == 1 {
+ // No handle found. Check if a handle for this path + a
+ // trailing slash exists
+ n = n.children[0]
+ if n.path == "/" && n.handlers != nil {
+ return append(ciPath, '/')
+ }
+ }
+
+ return nil
+
+ case catchAll:
+ return append(ciPath, path...)
+
+ default:
+ panic("invalid node type")
+ }
}
// Nothing found.
diff --git a/tree_test.go b/tree_test.go
index 1cb4f559..c3723396 100644
--- a/tree_test.go
+++ b/tree_test.go
@@ -33,6 +33,11 @@ func getParams() *Params {
return &ps
}
+func getSkippedNodes() *[]skippedNode {
+ ps := make([]skippedNode, 0, 20)
+ return &ps
+}
+
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false
if len(unescapes) >= 1 {
@@ -40,7 +45,7 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
for _, request := range requests {
- value := tree.getValue(request.path, getParams(), unescape)
+ value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape)
if value.handlers == nil {
if !request.nilHandler {
@@ -135,11 +140,16 @@ func TestTreeWildcard(t *testing.T) {
routes := [...]string{
"/",
- "/cmd/:tool/:sub",
"/cmd/:tool/",
+ "/cmd/:tool/:sub",
+ "/cmd/whoami",
+ "/cmd/whoami/root",
+ "/cmd/whoami/root/",
"/src/*filepath",
"/search/",
"/search/:query",
+ "/search/gin-gonic",
+ "/search/google",
"/user_:name",
"/user_:name/about",
"/files/:dir/*filepath",
@@ -148,6 +158,40 @@ func TestTreeWildcard(t *testing.T) {
"/doc/go1.html",
"/info/:user/public",
"/info/:user/project/:project",
+ "/info/:user/project/golang",
+ "/aa/*xx",
+ "/ab/*xx",
+ "/:cc",
+ "/c1/:dd/e",
+ "/c1/:dd/e1",
+ "/:cc/cc",
+ "/:cc/:dd/ee",
+ "/:cc/:dd/:ee/ff",
+ "/:cc/:dd/:ee/:ff/gg",
+ "/:cc/:dd/:ee/:ff/:gg/hh",
+ "/get/test/abc/",
+ "/get/:param/abc/",
+ "/something/:paramname/thirdthing",
+ "/something/secondthing/test",
+ "/get/abc",
+ "/get/:param",
+ "/get/abc/123abc",
+ "/get/abc/:param",
+ "/get/abc/123abc/xxx8",
+ "/get/abc/123abc/:param",
+ "/get/abc/123abc/xxx8/1234",
+ "/get/abc/123abc/xxx8/:param",
+ "/get/abc/123abc/xxx8/1234/ffas",
+ "/get/abc/123abc/xxx8/1234/:param",
+ "/get/abc/123abc/xxx8/1234/kkdd/12c",
+ "/get/abc/123abc/xxx8/1234/kkdd/:param",
+ "/get/abc/:param/test",
+ "/get/abc/123abd/:param",
+ "/get/abc/123abddd/:param",
+ "/get/abc/123/:param",
+ "/get/abc/123abg/:param",
+ "/get/abc/123abf/:param",
+ "/get/abc/123abfff/:param",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
@@ -155,19 +199,122 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
- {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
- {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
+ {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
+ {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
+ {"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
+ {"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
+ {"/cmd/whoami", false, "/cmd/whoami", nil},
+ {"/cmd/whoami/", true, "/cmd/whoami", nil},
+ {"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
+ {"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
+ {"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
+ {"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
+ {"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
+ {"/search/gin-gonic", false, "/search/gin-gonic", nil},
+ {"/search/google", false, "/search/google", nil},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
+ {"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
+ {"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
+ {"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
+ {"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
+ // * Error with argument being intercepted
+ // new PR handle (/all /all/cc /a/cc)
+ // fix PR: https://github.com/gin-gonic/gin/pull/2796
+ {"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
+ {"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
+ {"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
+ {"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
+ {"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
+ {"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
+ {"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
+ {"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
+ {"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
+ {"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
+ {"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
+ {"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
+ {"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
+ {"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
+ {"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
+ {"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}},
+ {"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}},
+ {"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}},
+ {"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
+ {"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
+ {"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
+ {"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
+ {"/get/test/abc/", false, "/get/test/abc/", nil},
+ {"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
+ {"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
+ {"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
+ {"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
+ {"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
+ {"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
+ {"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
+ {"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
+ {"/something/secondthing/test", false, "/something/secondthing/test", nil},
+ {"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
+ {"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
+ {"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
+ {"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
+ {"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
+ {"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
+ {"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
+ {"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
+ {"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
+ {"/get/abc", false, "/get/abc", nil},
+ {"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
+ {"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
+ {"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
+ {"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
+ {"/get/abc/123abc", false, "/get/abc/123abc", nil},
+ {"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
+ {"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
+ {"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
+ {"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
+ {"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
+ {"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
+ {"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
+ {"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
+ {"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
+ {"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
+ {"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
+ {"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
+ {"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
+ {"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
+ {"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
+ {"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
+ {"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
+ {"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
+ {"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
+ {"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
+ {"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
+ {"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
+ {"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
+ {"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
+ {"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
+ {"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
+ {"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
+ {"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
+ {"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
+ {"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
+ {"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
+ {"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
+ {"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
+ {"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
+ {"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
+ {"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
+ {"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
+ {"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
})
checkPriorities(t, tree)
@@ -245,20 +392,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
func TestTreeWildcardConflict(t *testing.T) {
routes := []testRoute{
{"/cmd/:tool/:sub", false},
- {"/cmd/vet", true},
+ {"/cmd/vet", false},
+ {"/foo/bar", false},
+ {"/foo/:name", false},
+ {"/foo/:names", true},
+ {"/cmd/*path", true},
+ {"/cmd/:badvar", true},
+ {"/cmd/:tool/names", false},
+ {"/cmd/:tool/:badsub/details", true},
{"/src/*filepath", false},
+ {"/src/:file", true},
+ {"/src/static.json", true},
{"/src/*filepathx", true},
{"/src/", true},
+ {"/src/foo/bar", true},
{"/src1/", false},
{"/src1/*filepath", true},
{"/src2*filepath", true},
+ {"/src2/*filepath", false},
{"/search/:query", false},
- {"/search/invalid", true},
+ {"/search/valid", false},
{"/user_:name", false},
- {"/user_x", true},
+ {"/user_x", false},
{"/user_:name", false},
{"/id:id", false},
- {"/id/:id", true},
+ {"/id/:id", false},
+ }
+ testRoutes(t, routes)
+}
+
+func TestCatchAllAfterSlash(t *testing.T) {
+ routes := []testRoute{
+ {"/non-leading-*catchall", true},
}
testRoutes(t, routes)
}
@@ -266,20 +431,23 @@ func TestTreeWildcardConflict(t *testing.T) {
func TestTreeChildConflict(t *testing.T) {
routes := []testRoute{
{"/cmd/vet", false},
- {"/cmd/:tool/:sub", true},
+ {"/cmd/:tool", false},
+ {"/cmd/:tool/:sub", false},
+ {"/cmd/:tool/misc", false},
+ {"/cmd/:tool/:othersub", true},
{"/src/AUTHORS", false},
{"/src/*filepath", true},
{"/user_x", false},
- {"/user_:name", true},
+ {"/user_:name", false},
{"/id/:id", false},
- {"/id:id", true},
- {"/:id", true},
+ {"/id:id", false},
+ {"/:id", false},
{"/*filepath", true},
}
testRoutes(t, routes)
}
-func TestTreeDupliatePath(t *testing.T) {
+func TestTreeDuplicatePath(t *testing.T) {
tree := &node{}
routes := [...]string{
@@ -419,7 +587,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/go1.html",
"/no/a",
"/no/b",
- "/api/hello/:name",
+ "/api/:page/:name",
+ "/api/hello/:name/bar/",
+ "/api/bar/:name",
+ "/api/baz/foo",
+ "/api/baz/foo/bar",
+ "/blog/:p",
+ "/posts/:b/:c",
+ "/posts/b/:c/d/",
}
for _, route := range routes {
recv := catchPanic(func() {
@@ -445,9 +620,21 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/admin/config/",
"/admin/config/permissions/",
"/doc/",
+ "/admin/static/",
+ "/admin/cfg/",
+ "/admin/cfg/users/",
+ "/api/hello/x/bar",
+ "/api/baz/foo/",
+ "/api/baz/bax/",
+ "/api/bar/huh/",
+ "/api/baz/foo/bar/",
+ "/api/world/abc/",
+ "/blog/pp/",
+ "/posts/b/c/d",
}
+
for _, route := range tsrRoutes {
- value := tree.getValue(route, nil, false)
+ value := tree.getValue(route, nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !value.tsr {
@@ -461,10 +648,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/no/",
"/_",
"/_/",
- "/api/world/abc",
+ "/api",
+ "/api/",
+ "/api/hello/x/foo",
+ "/api/baz/foo/bad",
+ "/foo/p/p",
}
for _, route := range noTsrRoutes {
- value := tree.getValue(route, nil, false)
+ value := tree.getValue(route, nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if value.tsr {
@@ -483,7 +674,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv)
}
- value := tree.getValue("/", nil, false)
+ value := tree.getValue("/", nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler")
} else if value.tsr {
@@ -663,7 +854,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
// normal lookup
recv := catchPanic(func() {
- tree.getValue("/test", nil, false)
+ tree.getValue("/test", nil, getSkippedNodes(), false)
})
if rs, ok := recv.(string); !ok || rs != panicMsg {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
@@ -678,6 +869,19 @@ 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
+
+ // set invalid Params type
+ params := make(Params, 0)
+
+ // try to trigger slice bounds out of range with capacity 0
+ tree.getValue("/test", ¶ms, getSkippedNodes(), false)
+}
+
func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct {
route string
@@ -688,8 +892,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
- {"/conxxx", "xxx", `/con:tact`, `:tact`},
- {"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
+ {"/con:nection", ":nection", `/con:tact`, `:tact`},
}
for _, conflict := range conflicts {
diff --git a/utils.go b/utils.go
index fab3aee3..c32f0eeb 100644
--- a/utils.go
+++ b/utils.go
@@ -103,7 +103,10 @@ func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
- if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
+ if i := strings.IndexByte(part, ';'); i > 0 {
+ part = part[:i]
+ }
+ if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
}
diff --git a/utils_test.go b/utils_test.go
index 9b57c57b..cc486c35 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -18,6 +18,12 @@ func init() {
SetMode(TestMode)
}
+func BenchmarkParseAccept(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
+ }
+}
+
type testStruct struct {
T *testing.T
}
diff --git a/version.go b/version.go
index 3e9687dc..b9110adb 100644
--- a/version.go
+++ b/version.go
@@ -5,4 +5,4 @@
package gin
// Version is the current gin framework's version.
-const Version = "v1.6.3"
+const Version = "v1.7.4"