Merge branch 'master' into feat/cookie-binding

This commit is contained in:
Bo-Yi Wu 2024-03-21 22:55:57 +08:00 committed by GitHub
commit 7e982b25b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1050 additions and 424 deletions

View File

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

View File

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

View File

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

View File

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

5
.gitignore vendored
View File

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

View File

@ -3,7 +3,6 @@ run:
linters:
enable:
- asciicheck
- depguard
- dogsled
- durationcheck
- errcheck
@ -19,6 +18,22 @@ linters:
- nolintlint
- revive
- wastedassign
linters-settings:
gosec:
# To select a subset of rules to run.
# Available rules: https://github.com/securego/gosec#available-rules
# Default: [] - means include all rules
includes:
- G102
- G106
- G108
- G109
- G111
- G112
- G201
- G203
issues:
exclude-rules:
- linters:
@ -37,3 +52,6 @@ issues:
- path: _test\.go
linters:
- gosec # security is not make sense in tests
- linters:
- revive
path: _test\.go

View File

@ -1,13 +1,70 @@
# Gin ChangeLog
## Gin v1.9.1
### BUG FIXES
* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
### SECURITY
* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
### ENHANCEMENTS
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
### DOCS
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
## Gin v1.9.0
### BREAK CHANGES
* Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150)
### BUG FIXES
* fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
* fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397)
* Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503)
### SECURITY
* Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
* fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
### ENHANCEMENTS
* feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
* chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316)
* fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327)
* remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395)
* refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433)
* console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453)
* chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456)
* chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464)
* chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
### DOCS
* docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
* docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400)
* docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449)
* docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446)
## Gin v1.8.2
### Bugs
### BUG FIXES
* fix(route): redirectSlash bug ([#3227]((https://github.com/gin-gonic/gin/pull/3227)))
* fix(engine): missing route params for CreateTestContext ([#2778]((https://github.com/gin-gonic/gin/pull/2778))) ([#2803]((https://github.com/gin-gonic/gin/pull/2803)))
### Security
### SECURITY
* Fix the GO-2022-1144 vulnerability ([#3432]((https://github.com/gin-gonic/gin/pull/3432)))
@ -19,12 +76,12 @@
## Gin v1.8.0
## Break Changes
### BREAK CHANGES
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP`
* gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
### BUGFIXES
### BUG FIXES
* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
* Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
@ -61,7 +118,7 @@
## Gin v1.7.7
### BUGFIXES
### BUG FIXES
* Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
* Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
@ -79,37 +136,37 @@
## Gin v1.7.6
### BUGFIXES
### BUG FIXES
* bump new release to fix v1.7.5 release error by using v1.7.4 codes.
## Gin v1.7.4
### BUGFIXES
### BUG FIXES
* bump new release to fix checksum mismatch
## Gin v1.7.3
### BUGFIXES
### BUG FIXES
* 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
### BUG FIXES
* 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
### BUG FIXES
* 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
### BUG FIXES
* 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))
@ -148,33 +205,44 @@
## Gin v1.6.2
### BUGFIXES
### BUG FIXES
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
### ENHANCEMENTS
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
## Gin v1.6.1
### BUGFIXES
### BUG FIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0
### BREAKING
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### BUGFIXES
### BUG FIXES
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
* Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
* fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
* Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
* [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
* Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
### ENHANCEMENTS
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
* tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
* tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
@ -189,7 +257,9 @@
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
* Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
* Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
### DOCS
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
* Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
* Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
@ -202,7 +272,9 @@
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
### MISC
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)

View File

@ -6,7 +6,6 @@
[![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)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
@ -31,7 +30,7 @@ Gin is a web framework written in [Go](https://go.dev/). It features a martini-l
### Prerequisites
- **[Go](https://go.dev/)**: ~~any one of the **three latest major** [releases](https://go.dev/doc/devel/release)~~ (now version **1.16+** is required).
- **[Go](https://go.dev/)**: any one of the **three latest major** [releases](https://go.dev/doc/devel/release) (we test it with these).
### Getting Gin
@ -176,4 +175,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
Gin is the work of hundreds of contributors. We appreciate your help!
Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.

10
any.go
View File

@ -1,10 +0,0 @@
// Copyright 2022 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.18
// +build !go1.18
package gin
type any = interface{}

25
auth.go
View File

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

View File

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

View File

@ -1,10 +0,0 @@
// Copyright 2022 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.18
// +build !go1.18
package binding
type any = interface{}

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding
@ -22,6 +21,7 @@ const (
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
)
@ -104,7 +104,7 @@ func Default(method, contentType string) Binding {
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
case MIMEYAML:
case MIMEYAML, MIMEYAML2:
return YAML
case MIMETOML:
return TOML

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build nomsgpack
// +build nomsgpack
package binding
@ -20,6 +19,7 @@ const (
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
)
@ -97,7 +97,7 @@ func Default(method, contentType string) Binding {
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEYAML:
case MIMEYAML, MIMEYAML2:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart

View File

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

View File

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

View File

@ -7,6 +7,7 @@ package binding
import (
"errors"
"fmt"
"mime/multipart"
"reflect"
"strconv"
"strings"
@ -235,10 +236,17 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
switch value.Interface().(type) {
case time.Time:
return setTimeField(val, field, value)
case multipart.FileHeader:
return nil
}
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr:
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
return setWithProperType(val, value.Elem(), field)
default:
return errUnknownType
}

View File

@ -5,6 +5,7 @@
package binding
import (
"mime/multipart"
"reflect"
"testing"
"time"
@ -43,6 +44,7 @@ func TestMappingBaseTypes(t *testing.T) {
{"zero value", struct{ F uint }{}, "", uint(0)},
{"zero value", struct{ F bool }{}, "", false},
{"zero value", struct{ F float32 }{}, "", float32(0)},
{"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}},
} {
tp := reflect.TypeOf(tt.value)
testName := tt.name + ":" + tp.Field(0).Type.String()
@ -269,6 +271,39 @@ func TestMappingStructField(t *testing.T) {
assert.Equal(t, 9, s.J.I)
}
func TestMappingPtrField(t *testing.T) {
type ptrStruct struct {
Key int64 `json:"key"`
}
type ptrRequest struct {
Items []*ptrStruct `json:"items" form:"items"`
}
var err error
// With 0 items.
var req0 ptrRequest
err = mappingByPtr(&req0, formSource{}, "form")
assert.NoError(t, err)
assert.Empty(t, req0.Items)
// With 1 item.
var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
assert.NoError(t, err)
assert.Len(t, req1.Items, 1)
assert.EqualValues(t, 1, req1.Items[0].Key)
// With 2 items.
var req2 ptrRequest
err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form")
assert.NoError(t, err)
assert.Len(t, req2.Items, 2)
assert.EqualValues(t, 1, req2.Items[0].Key)
assert.EqualValues(t, 2, req2.Items[1].Key)
}
func TestMappingMapField(t *testing.T) {
var s struct {
M map[string]int

View File

@ -15,7 +15,7 @@ import (
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
// interface{} as a Number instead of as a float64.
// any as a Number instead of as a float64.
var EnableDecoderUseNumber = false
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package binding

View File

@ -12,9 +12,9 @@ func (queryBinding) Name() string {
return "query"
}
func (queryBinding) Bind(req *http.Request, obj any) error {
func (q queryBinding) Bind(req *http.Request, obj any) error {
values := req.URL.Query()
if err := mapForm(obj, values); err != nil {
if err := mapFormByTag(obj, values, q.Name()); err != nil {
return err
}
return validate(obj)

23
binding/query_test.go Normal file
View File

@ -0,0 +1,23 @@
package binding
import (
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQueryBinding(t *testing.T) {
var s struct {
Foo string `query:"foo"`
}
request := &http.Request{URL: &url.URL{RawQuery: "foo=BAR"}}
err := queryBinding{}.Bind(request, &s)
require.NoError(t, err)
assert.Equal(t, "BAR", s.Foo)
}

View File

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

View File

@ -1,5 +1,13 @@
coverage:
notify:
gitter:
require_ci_to_pass: true
status:
project:
default:
url: https://webhooks.gitter.im/e/d90dcdeeab2f1e357165
target: 99%
threshold: 99%
patch:
default:
target: 99%
threshold: 95%

View File

@ -43,6 +43,10 @@ const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
// ContextKey is the key that a Context returns itself for.
const ContextKey = "_gin-gonic/gin/contextkey"
type ContextKeyType int
const ContextRequestKey ContextKeyType = 0
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1
@ -113,20 +117,27 @@ func (c *Context) Copy() *Context {
cp := Context{
writermem: c.writermem,
Request: c.Request,
Params: c.Params,
engine: c.engine,
}
cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem
cp.index = abortIndex
cp.handlers = nil
cp.Keys = map[string]any{}
for k, v := range c.Keys {
cp.fullPath = c.fullPath
cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
c.mu.RLock()
for k, v := range cKeys {
cp.Keys[k] = v
}
paramCopy := make([]Param, len(cp.Params))
copy(paramCopy, cp.Params)
cp.Params = paramCopy
c.mu.RUnlock()
cParams := c.Params
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
return &cp
}
@ -386,7 +397,7 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
//
// router.GET("/user/:id", func(c *gin.Context) {
// // a GET request to /user/john
// id := c.Param("id") // id == "/john"
// id := c.Param("id") // id == "john"
// // a GET request to /user/john/
// id := c.Param("id") // id == "/john/"
// })
@ -652,7 +663,7 @@ func (c *Context) BindYAML(obj any) error {
}
// BindTOML is a shortcut for c.MustBindWith(obj, binding.TOML).
func (c *Context) BindTOML(obj interface{}) error {
func (c *Context) BindTOML(obj any) error {
return c.MustBindWith(obj, binding.TOML)
}
@ -717,7 +728,7 @@ func (c *Context) ShouldBindYAML(obj any) error {
}
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
func (c *Context) ShouldBindTOML(obj interface{}) error {
func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML)
}
@ -878,6 +889,9 @@ func (c *Context) GetHeader(key string) string {
// GetRawData returns stream data.
func (c *Context) GetRawData() ([]byte, error) {
if c.Request.Body == nil {
return nil, errors.New("cannot read nil body")
}
return io.ReadAll(c.Request.Body)
}
@ -1000,7 +1014,7 @@ func (c *Context) YAML(code int, obj any) {
}
// TOML serializes the given struct as TOML into the response body.
func (c *Context) TOML(code int, obj interface{}) {
func (c *Context) TOML(code int, obj any) {
c.Render(code, render.TOML{Data: obj})
}
@ -1057,11 +1071,17 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
}
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
// FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) {
if isASCII(filename) {
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+escapeQuotes(filename)+`"`)
} else {
c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename))
}
@ -1179,9 +1199,16 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
// hasRequestContext returns whether c.Request has Context and fallback.
func (c *Context) hasRequestContext() bool {
hasFallback := c.engine != nil && c.engine.ContextWithFallback
hasRequestContext := c.Request != nil && c.Request.Context() != nil
return hasFallback && hasRequestContext
}
// Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
if !c.hasRequestContext() {
return
}
return c.Request.Context().Deadline()
@ -1189,7 +1216,7 @@ func (c *Context) Deadline() (deadline time.Time, ok bool) {
// Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} {
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
if !c.hasRequestContext() {
return nil
}
return c.Request.Context().Done()
@ -1197,7 +1224,7 @@ func (c *Context) Done() <-chan struct{} {
// Err returns nil when c.Request has no Context.
func (c *Context) Err() error {
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
if !c.hasRequestContext() {
return nil
}
return c.Request.Context().Err()
@ -1207,7 +1234,7 @@ func (c *Context) Err() error {
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
func (c *Context) Value(key any) any {
if key == 0 {
if key == ContextRequestKey {
return c.Request
}
if key == ContextKey {
@ -1218,7 +1245,7 @@ func (c *Context) Value(key any) any {
return val
}
}
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
if !c.hasRequestContext() {
return nil
}
return c.Request.Context().Value(key)

View File

@ -1,94 +0,0 @@
// 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"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type interceptedWriter struct {
ResponseWriter
b *bytes.Buffer
}
func (i interceptedWriter) WriteHeader(code int) {
i.Header().Del("X-Test")
i.ResponseWriter.WriteHeader(code)
}
func TestContextFormFileFailed17(t *testing.T) {
if !isGo117OrGo118() {
return
}
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
defer func(mw *multipart.Writer) {
err := mw.Close()
if err != nil {
assert.Error(t, err)
}
}(mw)
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)
})
}
func TestInterceptedHeader(t *testing.T) {
w := httptest.NewRecorder()
c, r := CreateTestContext(w)
r.Use(func(c *Context) {
i := interceptedWriter{
ResponseWriter: c.Writer,
b: bytes.NewBuffer(nil),
}
c.Writer = i
c.Next()
c.Header("X-Test", "overridden")
c.Writer = i.ResponseWriter
})
r.GET("/", func(c *Context) {
c.Header("X-Test", "original")
c.Header("X-Test-2", "present")
c.String(http.StatusOK, "hello world")
})
c.Request = httptest.NewRequest("GET", "/", nil)
r.HandleContext(c)
// Result() has headers frozen when WriteHeaderNow() has been called
// Compared to this time, this is when the response headers will be flushed
// As response is flushed on c.String, the Header cannot be set by the first
// middleware. Assert this
assert.Equal(t, "", w.Result().Header.Get("X-Test"))
assert.Equal(t, "present", w.Result().Header.Get("X-Test-2"))
}
func isGo117OrGo118() bool {
version := strings.Split(runtime.Version()[2:], ".")
if len(version) >= 2 {
x := version[0]
y := version[1]
if x == "1" && (y == "17" || y == "18") {
return true
}
}
return false
}

View File

@ -2,8 +2,7 @@
// 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
//go:build !go1.19
package gin
@ -17,15 +16,22 @@ import (
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed16(t *testing.T) {
func TestContextFormFileFailed18(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
defer func(mw *multipart.Writer) {
err := mw.Close()
if err != nil {
assert.Error(t, err)
}
}(mw)
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)
assert.Panics(t, func() {
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
})
}

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package gin

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build appengine
// +build appengine
package gin

View File

@ -37,7 +37,7 @@ var errTestRender = errors.New("TestRender")
// Unit tests TODO
// func (c *Context) File(filepath string) {
// func (c *Context) Negotiate(code int, config Negotiate) {
// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
// BAD case: func (c *Context) Render(code int, render render.Render, obj ...any) {
// test that information is not leaked when reusing Contexts (using the Pool)
func createMultipartRequest() *http.Request {
@ -324,6 +324,7 @@ func TestContextCopy(t *testing.T) {
c.handlers = HandlersChain{func(c *Context) {}}
c.Params = Params{Param{Key: "foo", Value: "bar"}}
c.Set("foo", "bar")
c.fullPath = "/hola"
cp := c.Copy()
assert.Nil(t, cp.handlers)
@ -336,6 +337,7 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.Params, c.Params)
cp.Set("foo", "notBar")
assert.False(t, cp.Keys["foo"] == c.Keys["foo"])
assert.Equal(t, cp.fullPath, c.fullPath)
}
func TestContextHandlerName(t *testing.T) {
@ -998,7 +1000,7 @@ func TestContextRenderFile(t *testing.T) {
c.File("./gin.go")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
@ -1012,7 +1014,7 @@ func TestContextRenderFileFromFS(t *testing.T) {
c.FileFromFS("./gin.go", Dir(".", false))
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
@ -1028,10 +1030,24 @@ func TestContextRenderAttachment(t *testing.T) {
c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
}
func TestContextRenderAndEscapeAttachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
maliciousFilename := "tampering_field.sh\"; \\\"; dummy=.go"
actualEscapedResponseFilename := "tampering_field.sh\\\"; \\\\\\\"; dummy=.go"
c.Request, _ = http.NewRequest("GET", "/", nil)
c.FileAttachment("./gin.go", maliciousFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", actualEscapedResponseFilename), w.Header().Get("Content-Disposition"))
}
func TestContextRenderUTF8Attachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1041,12 +1057,12 @@ func TestContextRenderUTF8Attachment(t *testing.T) {
c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Contains(t, w.Body.String(), "func New(opts ...OptionFunc) *Engine {")
assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition"))
}
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml
// and Content-Type is set to application/yaml
func TestContextRenderYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1055,7 +1071,7 @@ func TestContextRenderYAML(t *testing.T) {
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextRenderTOML tests that the response is serialized as TOML
@ -1203,7 +1219,7 @@ func TestContextNegotiationWithYAML(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithTOML(t *testing.T) {
@ -1555,6 +1571,12 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Del("CF-Connecting-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = PlatformFlyIO
assert.Equal(t, "70.70.70.70", c.ClientIP())
c.Request.Header.Del("Fly-Client-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = ""
// no port
@ -1567,6 +1589,7 @@ func resetContextForClientIPTests(c *Context) {
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
c.Request.Header.Set("Fly-Client-IP", "70.70.70.70")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
c.engine.TrustedPlatform = ""
c.engine.trustedCIDRs = defaultTrustedCIDRs
@ -1983,7 +2006,7 @@ func TestContextGolangContext(t *testing.T) {
ti, ok := c.Deadline()
assert.Equal(t, ti, time.Time{})
assert.False(t, ok)
assert.Equal(t, c.Value(0), c.Request)
assert.Equal(t, c.Value(ContextRequestKey), c.Request)
assert.Equal(t, c.Value(ContextKey), c)
assert.Nil(t, c.Value("foo"))
@ -2183,6 +2206,24 @@ func TestRemoteIPFail(t *testing.T) {
assert.False(t, trust)
}
func TestHasRequestContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.hasRequestContext(), "no request, no fallback")
c.engine.ContextWithFallback = true
assert.False(t, c.hasRequestContext(), "no request, has fallback")
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
assert.True(t, c.hasRequestContext(), "has request, has fallback")
c.Request, _ = http.NewRequestWithContext(nil, "", "", nil) //nolint:staticcheck
assert.False(t, c.hasRequestContext(), "has request with nil ctx, has fallback")
c.engine.ContextWithFallback = false
assert.False(t, c.hasRequestContext(), "has request, no fallback")
c = &Context{}
assert.False(t, c.hasRequestContext(), "no request, no engine")
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
assert.False(t, c.hasRequestContext(), "has request, no engine")
}
func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
@ -2395,3 +2436,42 @@ func TestCreateTestContextWithRouteParams(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "hello gin", w.Body.String())
}
type interceptedWriter struct {
ResponseWriter
b *bytes.Buffer
}
func (i interceptedWriter) WriteHeader(code int) {
i.Header().Del("X-Test")
i.ResponseWriter.WriteHeader(code)
}
func TestInterceptedHeader(t *testing.T) {
w := httptest.NewRecorder()
c, r := CreateTestContext(w)
r.Use(func(c *Context) {
i := interceptedWriter{
ResponseWriter: c.Writer,
b: bytes.NewBuffer(nil),
}
c.Writer = i
c.Next()
c.Header("X-Test", "overridden")
c.Writer = i.ResponseWriter
})
r.GET("/", func(c *Context) {
c.Header("X-Test", "original")
c.Header("X-Test-2", "present")
c.String(http.StatusOK, "hello world")
})
c.Request = httptest.NewRequest("GET", "/", nil)
r.HandleContext(c)
// Result() has headers frozen when WriteHeaderNow() has been called
// Compared to this time, this is when the response headers will be flushed
// As response is flushed on c.String, the Header cannot be set by the first
// middleware. Assert this
assert.Equal(t, "", w.Result().Header.Get("X-Test"))
assert.Equal(t, "present", w.Result().Header.Get("X-Test-2"))
}

View File

@ -12,7 +12,7 @@ import (
"strings"
)
const ginSupportMinGoVer = 16
const ginSupportMinGoVer = 18
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -23,6 +23,9 @@ func IsDebugging() bool {
// DebugPrintRouteFunc indicates debug log output format.
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
// DebugPrintFunc indicates debug log output format.
var DebugPrintFunc func(format string, values ...interface{})
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() {
nuHandlers := len(handlers)
@ -48,12 +51,19 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
}
func debugPrint(format string, values ...any) {
if IsDebugging() {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
if !IsDebugging() {
return
}
if DebugPrintFunc != nil {
DebugPrintFunc(format, values...)
return
}
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
func getMinVer(v string) (uint64, error) {
@ -67,7 +77,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.16+.
debugPrint(`[WARNING] Now Gin requires Go 1.18+.
`)
}

View File

@ -21,7 +21,7 @@ import (
// TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...interface{}) {
// func debugPrint(format string, values ...any) {
func TestIsDebugging(t *testing.T) {
SetMode(DebugMode)
@ -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.16+.\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.18+.\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)
}

View File

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

View File

@ -426,7 +426,7 @@ func main() {
r.Use(gin.Logger())
// Recovery middleware recovers from any panics and writes a 500 if there was one.
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered any) {
if err, ok := recovered.(string); ok {
c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
}
@ -509,6 +509,44 @@ Sample Output
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
```
### Skip logging
```go
func main() {
router := gin.New()
// skip logging for desired paths by setting SkipPaths in LoggerConfig
loggerConfig := gin.LoggerConfig{SkipPaths: []string{"/metrics"}}
// skip logging based on your logic by setting Skip func in LoggerConfig
loggerConfig.Skip = func(c *gin.Context) bool {
// as an example skip non server side errors
return c.Writer.Status() < http.StatusInternalServerError
}
engine.Use(gin.LoggerWithConfig(loggerConfig))
router.Use(gin.Recovery())
// skipped
router.GET("/metrics", func(c *gin.Context) {
c.Status(http.StatusNotImplemented)
})
// skipped
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// not skipped
router.GET("/data", func(c *gin.Context) {
c.Status(http.StatusNotImplemented)
})
router.Run(":8080")
}
```
### Controlling Log output coloring
By default, logs output on console should be colorized depending on the detected TTY.
@ -1036,7 +1074,7 @@ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:
func main() {
r := gin.Default()
// gin.H is a shortcut for map[string]interface{}
// gin.H is a shortcut for map[string]any
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
@ -2001,7 +2039,7 @@ func (customerBinding) Name() string {
return "form"
}
func (customerBinding) Bind(req *http.Request, obj interface{}) error {
func (customerBinding) Bind(req *http.Request, obj any) error {
if err := req.ParseForm(); err != nil {
return err
}
@ -2016,7 +2054,7 @@ func (customerBinding) Bind(req *http.Request, obj interface{}) error {
return validate(obj)
}
func validate(obj interface{}) error {
func validate(obj any) error {
if binding.Validator == nil {
return nil
}
@ -2216,10 +2254,16 @@ import (
func main() {
router := gin.Default()
// Use predefined header gin.PlatformXXX
// Google App Engine
router.TrustedPlatform = gin.PlatformGoogleAppEngine
// Or set your own trusted request header for another trusted proxy service
// Don't set it to any suspect request header, it's unsafe
router.TrustedPlatform = "X-CDN-IP"
// Cloudflare
router.TrustedPlatform = gin.PlatformCloudflare
// Fly.io
router.TrustedPlatform = gin.PlatformFlyIO
// Or, you can set your own trusted request header. But be sure your CDN
// prevents users from passing this header! For example, if your CDN puts
// the client IP in X-CDN-Client-IP:
router.TrustedPlatform = "X-CDN-Client-IP"
router.GET("/", func(c *gin.Context) {
// If you set TrustedPlatform, ClientIP() will resolve the

2
fs.go
View File

@ -39,7 +39,7 @@ func (fs onlyFilesFS) Open(name string) (http.File, error) {
}
// Readdir overrides the http.File default implementation.
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
func (f neuteredReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
// this disables directory listing
return nil, nil
}

50
gin.go
View File

@ -11,6 +11,7 @@ import (
"net/http"
"os"
"path"
"regexp"
"strings"
"sync"
@ -40,9 +41,15 @@ var defaultTrustedCIDRs = []*net.IPNet{
},
}
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// OptionFunc defines the function to change the default configuration
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc
@ -73,6 +80,8 @@ const (
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP
PlatformCloudflare = "CF-Connecting-IP"
// PlatformFlyIO when running on Fly.io. Trust Fly-Client-IP for determining the client's IP
PlatformFlyIO = "Fly-Client-IP"
)
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
@ -176,7 +185,7 @@ var _ IRouter = (*Engine)(nil)
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
@ -205,15 +214,15 @@ func New() *Engine {
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
return engine.With(opts...)
}
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
return engine.With(opts...)
}
func (engine *Engine) Handler() http.Handler {
@ -307,6 +316,15 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine
}
// With returns a new Engine instance with the provided options.
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
}
return engine
}
func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
}
@ -330,7 +348,6 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
}
root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
@ -511,7 +528,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
listener, err := net.Listen("unix", file)
@ -534,7 +551,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
@ -555,7 +572,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.Serve(listener, engine.Handler())
@ -630,17 +647,25 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
if engine.HandleMethodNotAllowed {
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
// containing a list of the target resource's currently supported methods.
allowed := make([]string, 0, len(t)-1)
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
allowed = append(allowed, tree.method)
}
}
if len(allowed) > 0 {
c.handlers = engine.allNoMethod
c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
@ -668,6 +693,9 @@ func redirectTrailingSlash(c *Context) {
req := c.Request
p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
prefix = regSafePrefix.ReplaceAllString(prefix, "")
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
p = prefix + "/" + req.URL.Path
}
req.URL.Path = p + "/"

View File

@ -696,3 +696,37 @@ func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo)
func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
}
r := New(fc)
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
}
func TestWithOptionFunc(t *testing.T) {
r := New()
r.With(func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
})
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
}

39
go.mod
View File

@ -1,36 +1,37 @@
module github.com/gin-gonic/gin
go 1.18
go 1.20
require (
github.com/bytedance/sonic v1.7.1
github.com/bytedance/sonic v1.11.0
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.11.2
github.com/goccy/go-json v0.10.0
github.com/go-playground/validator/v10 v10.18.0
github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.17
github.com/pelletier/go-toml/v2 v2.0.6
github.com/stretchr/testify v1.8.1
github.com/ugorji/go/codec v1.2.9
golang.org/x/net v0.5.0
google.golang.org/protobuf v1.28.1
github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.1.1
github.com/stretchr/testify v1.8.4
github.com/ugorji/go/codec v1.2.12
golang.org/x/net v0.21.0
google.golang.org/protobuf v1.33.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.14 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.0.0-20220412001346-fc48f9fe4c15 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

88
go.sum
View File

@ -1,13 +1,19 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.7.1 h1:UYWEKUHQDye89c2U6zvrvuxWdGCI/wCrZITFQmKGtGc=
github.com/bytedance/sonic v1.7.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo=
github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -15,69 +21,65 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.14 h1:QRqdp6bb9M9S5yyKeYteXKuoKE4p0tGlra81fKOpWH8=
github.com/klauspost/cpuid/v2 v2.0.14/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.0.0-20220412001346-fc48f9fe4c15 h1:GVfVkciLYxn5mY5EncwAe0SXUn9Rm81rRkZ0TTmn/cU=
golang.org/x/arch v0.0.0-20220412001346-fc48f9fe4c15/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.20
package bytesconv
import (

View File

@ -0,0 +1,23 @@
// Copyright 2023 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.20
package bytesconv
import (
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// BytesToString converts byte slice to string without a memory allocation.
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go_json
// +build go_json
package json

View File

@ -3,9 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
// +build !jsoniter
// +build !go_json
// +build !sonic !avx !linux,!windows,!darwin !amd64
package json

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build jsoniter
// +build jsoniter
package json

View File

@ -3,10 +3,6 @@
// license that can be found in the LICENSE file.
//go:build sonic && avx && (linux || windows || darwin) && amd64
// +build sonic
// +build avx
// +build linux windows darwin
// +build amd64
package json

View File

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

View File

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

View File

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

View File

@ -103,7 +103,7 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
}
func defaultHandleRecovery(c *Context, err any) {
func defaultHandleRecovery(c *Context, _ any) {
c.AbortWithStatus(http.StatusInternalServerError)
}
@ -164,7 +164,7 @@ func function(pc uintptr) []byte {
if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:]
}
name = bytes.Replace(name, centerDot, dot, -1)
name = bytes.ReplaceAll(name, centerDot, dot)
return name
}

View File

@ -1,10 +0,0 @@
// 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.18
// +build !go1.18
package render
type any = interface{}

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package render

View File

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

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !nomsgpack
// +build !nomsgpack
package render

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
// 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.18
// +build !go1.18
package protoexample
type any = interface{}

43
tree.go
View File

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

View File

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

View File

@ -50,7 +50,7 @@ func WrapH(h http.Handler) HandlerFunc {
}
}
// H is a shortcut for map[string]interface{}
// H is a shortcut for map[string]any
type H map[string]any
// MarshalXML allows type H to be used with xml.Marshal.

View File

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