mirror of https://github.com/gin-gonic/gin.git
Merge branch 'master' into feat/cookie-binding
This commit is contained in:
commit
7e982b25b9
|
@ -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>
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,4 +5,7 @@ count.out
|
|||
test
|
||||
profile.out
|
||||
tmp.out
|
||||
.idea
|
||||
|
||||
# Develop tools
|
||||
.idea/
|
||||
.vscode/
|
||||
|
|
|
@ -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
|
||||
|
|
100
CHANGELOG.md
100
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
10
any.go
10
any.go
|
@ -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
25
auth.go
|
@ -16,6 +16,9 @@ import (
|
|||
// AuthUserKey is the cookie name for user credential in basic auth.
|
||||
const AuthUserKey = "user"
|
||||
|
||||
// AuthProxyUserKey is the cookie name for proxy_user credential in basic auth for proxy.
|
||||
const AuthProxyUserKey = "proxy_user"
|
||||
|
||||
// Accounts defines a key/value for user/pass list of authorized logins.
|
||||
type Accounts map[string]string
|
||||
|
||||
|
@ -89,3 +92,25 @@ func authorizationHeader(user, password string) string {
|
|||
base := user + ":" + password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
|
||||
}
|
||||
|
||||
// BasicAuthForProxy returns a Basic HTTP Proxy-Authorization middleware.
|
||||
// If the realm is empty, "Proxy Authorization Required" will be used by default.
|
||||
func BasicAuthForProxy(accounts Accounts, realm string) HandlerFunc {
|
||||
if realm == "" {
|
||||
realm = "Proxy Authorization Required"
|
||||
}
|
||||
realm = "Basic realm=" + strconv.Quote(realm)
|
||||
pairs := processAccounts(accounts)
|
||||
return func(c *Context) {
|
||||
proxyUser, found := pairs.searchCredential(c.requestHeader("Proxy-Authorization"))
|
||||
if !found {
|
||||
// Credentials doesn't match, we return 407 and abort handlers chain.
|
||||
c.Header("Proxy-Authenticate", realm)
|
||||
c.AbortWithStatus(http.StatusProxyAuthRequired)
|
||||
return
|
||||
}
|
||||
// The proxy_user credentials was found, set proxy_user's id to key AuthProxyUserKey in this context, the proxy_user's id can be read later using
|
||||
// c.MustGet(gin.AuthProxyUserKey).
|
||||
c.Set(AuthProxyUserKey, proxyUser)
|
||||
}
|
||||
}
|
||||
|
|
37
auth_test.go
37
auth_test.go
|
@ -137,3 +137,40 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
|||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
|
||||
}
|
||||
|
||||
func TestBasicAuthForProxySucceed(t *testing.T) {
|
||||
accounts := Accounts{"admin": "password"}
|
||||
router := New()
|
||||
router.Use(BasicAuthForProxy(accounts, ""))
|
||||
router.Any("/*proxyPath", func(c *Context) {
|
||||
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "admin", w.Body.String())
|
||||
}
|
||||
|
||||
func TestBasicAuthForProxy407(t *testing.T) {
|
||||
called := false
|
||||
accounts := Accounts{"foo": "bar"}
|
||||
router := New()
|
||||
router.Use(BasicAuthForProxy(accounts, ""))
|
||||
router.Any("/*proxyPath", func(c *Context) {
|
||||
called = true
|
||||
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.False(t, called)
|
||||
assert.Equal(t, http.StatusProxyAuthRequired, w.Code)
|
||||
assert.Equal(t, "Basic realm=\"Proxy Authorization Required\"", w.Header().Get("Proxy-Authenticate"))
|
||||
}
|
||||
|
|
|
@ -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{}
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !nomsgpack
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -164,6 +164,8 @@ func TestBindingDefault(t *testing.T) {
|
|||
|
||||
assert.Equal(t, YAML, Default("POST", MIMEYAML))
|
||||
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
|
||||
assert.Equal(t, YAML, Default("POST", MIMEYAML2))
|
||||
assert.Equal(t, YAML, Default("PUT", MIMEYAML2))
|
||||
|
||||
assert.Equal(t, TOML, Default("POST", MIMETOML))
|
||||
assert.Equal(t, TOML, Default("PUT", MIMETOML))
|
||||
|
|
|
@ -54,7 +54,10 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
|||
value := reflect.ValueOf(obj)
|
||||
switch value.Kind() {
|
||||
case reflect.Ptr:
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !nomsgpack
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !nomsgpack
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
|
|
14
codecov.yml
14
codecov.yml
|
@ -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%
|
59
context.go
59
context.go
|
@ -43,6 +43,10 @@ const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
|
|||
// ContextKey is the key that a Context returns itself for.
|
||||
const ContextKey = "_gin-gonic/gin/contextkey"
|
||||
|
||||
type ContextKeyType int
|
||||
|
||||
const ContextRequestKey ContextKeyType = 0
|
||||
|
||||
// abortIndex represents a typical value used in abort functions.
|
||||
const abortIndex int8 = math.MaxInt8 >> 1
|
||||
|
||||
|
@ -113,20 +117,27 @@ func (c *Context) Copy() *Context {
|
|||
cp := Context{
|
||||
writermem: c.writermem,
|
||||
Request: c.Request,
|
||||
Params: c.Params,
|
||||
engine: c.engine,
|
||||
}
|
||||
|
||||
cp.writermem.ResponseWriter = nil
|
||||
cp.Writer = &cp.writermem
|
||||
cp.index = abortIndex
|
||||
cp.handlers = nil
|
||||
cp.Keys = map[string]any{}
|
||||
for k, v := range c.Keys {
|
||||
cp.fullPath = c.fullPath
|
||||
|
||||
cKeys := c.Keys
|
||||
cp.Keys = make(map[string]any, len(cKeys))
|
||||
c.mu.RLock()
|
||||
for k, v := range cKeys {
|
||||
cp.Keys[k] = v
|
||||
}
|
||||
paramCopy := make([]Param, len(cp.Params))
|
||||
copy(paramCopy, cp.Params)
|
||||
cp.Params = paramCopy
|
||||
c.mu.RUnlock()
|
||||
|
||||
cParams := c.Params
|
||||
cp.Params = make([]Param, len(cParams))
|
||||
copy(cp.Params, cParams)
|
||||
|
||||
return &cp
|
||||
}
|
||||
|
||||
|
@ -386,7 +397,7 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
|
|||
//
|
||||
// router.GET("/user/:id", func(c *gin.Context) {
|
||||
// // a GET request to /user/john
|
||||
// id := c.Param("id") // id == "/john"
|
||||
// id := c.Param("id") // id == "john"
|
||||
// // a GET request to /user/john/
|
||||
// id := c.Param("id") // id == "/john/"
|
||||
// })
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package gin
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build appengine
|
||||
// +build appengine
|
||||
|
||||
package gin
|
||||
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
24
debug.go
24
debug.go
|
@ -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+.
|
||||
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.`)
|
||||
|
|
58
docs/doc.md
58
docs/doc.md
|
@ -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
2
fs.go
|
@ -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
50
gin.go
|
@ -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 + "/"
|
||||
|
|
34
gin_test.go
34
gin_test.go
|
@ -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
39
go.mod
|
@ -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
88
go.sum
|
@ -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=
|
||||
|
|
|
@ -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 (
|
|
@ -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))
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go_json
|
||||
// +build go_json
|
||||
|
||||
package json
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build jsoniter
|
||||
// +build jsoniter
|
||||
|
||||
package json
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
63
logger.go
63
logger.go
|
@ -47,8 +47,15 @@ type LoggerConfig struct {
|
|||
// SkipPaths is an url path array which logs are not written.
|
||||
// Optional.
|
||||
SkipPaths []string
|
||||
|
||||
// Skip is a Skipper that indicates which logs should not be written.
|
||||
// Optional.
|
||||
Skip Skipper
|
||||
}
|
||||
|
||||
// Skipper is a function to skip logs based on provided Context
|
||||
type Skipper func(c *Context) bool
|
||||
|
||||
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
|
||||
type LogFormatter func(params LogFormatterParams) string
|
||||
|
||||
|
@ -83,6 +90,8 @@ func (p *LogFormatterParams) StatusCodeColor() string {
|
|||
code := p.StatusCode
|
||||
|
||||
switch {
|
||||
case code >= http.StatusContinue && code < http.StatusOK:
|
||||
return white
|
||||
case code >= http.StatusOK && code < http.StatusMultipleChoices:
|
||||
return green
|
||||
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
|
||||
|
@ -239,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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,6 +310,7 @@ func TestColorForStatus(t *testing.T) {
|
|||
return p.StatusCodeColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, white, colorForStatus(http.StatusContinue), "1xx should be white")
|
||||
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
|
||||
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||
|
@ -414,6 +415,26 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
|||
assert.Contains(t, buffer.String(), "")
|
||||
}
|
||||
|
||||
func TestLoggerWithConfigSkipper(t *testing.T) {
|
||||
buffer := new(strings.Builder)
|
||||
router := New()
|
||||
router.Use(LoggerWithConfig(LoggerConfig{
|
||||
Output: buffer,
|
||||
Skip: func(c *Context) bool {
|
||||
return c.Writer.Status() == http.StatusNoContent
|
||||
},
|
||||
}))
|
||||
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
|
||||
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
|
||||
|
||||
PerformRequest(router, "GET", "/logged")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "GET", "/skipped")
|
||||
assert.Contains(t, buffer.String(), "")
|
||||
}
|
||||
|
||||
func TestDisableConsoleColor(t *testing.T) {
|
||||
New()
|
||||
assert.Equal(t, autoColor, consoleColorMode)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package gin
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -80,6 +81,10 @@ func TestPathCleanMallocs(t *testing.T) {
|
|||
t.Skip("skipping malloc count in short mode")
|
||||
}
|
||||
|
||||
if runtime.GOMAXPROCS(0) > 1 {
|
||||
t.Skip("skipping malloc count; GOMAXPROCS>1")
|
||||
}
|
||||
|
||||
for _, test := range cleanTests {
|
||||
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
|
||||
assert.EqualValues(t, allocs, 0)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{}
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !nomsgpack
|
||||
// +build !nomsgpack
|
||||
|
||||
package render
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !nomsgpack
|
||||
// +build !nomsgpack
|
||||
|
||||
package render
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
@ -136,6 +137,51 @@ func TestRenderJsonpJSON(t *testing.T) {
|
|||
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
type errorWriter struct {
|
||||
bufString string
|
||||
*httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*errorWriter)(nil)
|
||||
|
||||
func (w *errorWriter) Write(buf []byte) (int, error) {
|
||||
if string(buf) == w.bufString {
|
||||
return 0, errors.New(`write "` + w.bufString + `" error`)
|
||||
}
|
||||
return w.ResponseRecorder.Write(buf)
|
||||
}
|
||||
|
||||
func TestRenderJsonpJSONError(t *testing.T) {
|
||||
ew := &errorWriter{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
|
||||
jsonpJSON := JsonpJSON{
|
||||
Callback: "foo",
|
||||
Data: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
cb := template.JSEscapeString(jsonpJSON.Callback)
|
||||
ew.bufString = cb
|
||||
err := jsonpJSON.Render(ew) // error was returned while writing callback
|
||||
assert.Equal(t, `write "`+cb+`" error`, err.Error())
|
||||
|
||||
ew.bufString = `(`
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
||||
|
||||
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||
ew.bufString = string(data)
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
||||
|
||||
ew.bufString = `);`
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+`);`+`" error`, err.Error())
|
||||
}
|
||||
|
||||
func TestRenderJsonpJSONError2(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{
|
||||
|
@ -234,12 +280,12 @@ b:
|
|||
d: [3, 4]
|
||||
`
|
||||
(YAML{data}).WriteContentType(w)
|
||||
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
err := (YAML{data}).Render(w)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
|
||||
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
type fail struct{}
|
||||
|
@ -532,3 +578,16 @@ func TestRenderReaderNoContentLength(t *testing.T) {
|
|||
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
||||
}
|
||||
|
||||
func TestRenderWriteError(t *testing.T) {
|
||||
data := []interface{}{"value1", "value2"}
|
||||
prefix := "my-prefix:"
|
||||
r := SecureJSON{Data: data, Prefix: prefix}
|
||||
ew := &errorWriter{
|
||||
bufString: prefix,
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
err := r.Render(ew)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, `write "my-prefix:" error`, err.Error())
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ type YAML struct {
|
|||
Data any
|
||||
}
|
||||
|
||||
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
|
||||
var yamlContentType = []string{"application/yaml; charset=utf-8"}
|
||||
|
||||
// Render (YAML) marshals the given interface object and writes data with custom ContentType.
|
||||
func (r YAML) Render(w http.ResponseWriter) error {
|
||||
|
|
|
@ -156,3 +156,33 @@ func TestResponseWriterStatusCode(t *testing.T) {
|
|||
// status must be 200 although we tried to change it
|
||||
assert.Equal(t, http.StatusOK, w.Status())
|
||||
}
|
||||
|
||||
// mockPusherResponseWriter is an http.ResponseWriter that implements http.Pusher.
|
||||
type mockPusherResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (m *mockPusherResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nonPusherResponseWriter is an http.ResponseWriter that does not implement http.Pusher.
|
||||
type nonPusherResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func TestPusherWithPusher(t *testing.T) {
|
||||
rw := &mockPusherResponseWriter{}
|
||||
w := &responseWriter{ResponseWriter: rw}
|
||||
|
||||
pusher := w.Pusher()
|
||||
assert.NotNil(t, pusher, "Expected pusher to be non-nil")
|
||||
}
|
||||
|
||||
func TestPusherWithoutPusher(t *testing.T) {
|
||||
rw := &nonPusherResponseWriter{}
|
||||
w := &responseWriter{ResponseWriter: rw}
|
||||
|
||||
pusher := w.Pusher()
|
||||
assert.Nil(t, pusher, "Expected pusher to be nil")
|
||||
}
|
||||
|
|
113
routes_test.go
113
routes_test.go
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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
43
tree.go
|
@ -351,7 +351,10 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
|||
}
|
||||
|
||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||
pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]
|
||||
pathSeg := ""
|
||||
if len(n.children) != 0 {
|
||||
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
|
||||
}
|
||||
panic("catch-all wildcard '" + path +
|
||||
"' in new path '" + fullPath +
|
||||
"' conflicts with existing path segment '" + pathSeg +
|
||||
|
@ -478,7 +481,7 @@ walk: // Outer loop for walking the tree
|
|||
// We can recommend to redirect to the same URL without a
|
||||
// trailing slash if a leaf exists for that path.
|
||||
value.tsr = path == "/" && n.handlers != nil
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// Handle wildcard child, which is always at the end of the array
|
||||
|
@ -497,7 +500,14 @@ walk: // Outer loop for walking the tree
|
|||
}
|
||||
|
||||
// Save param value
|
||||
if params != nil && cap(*params) > 0 {
|
||||
if params != nil {
|
||||
// Preallocate capacity if necessary
|
||||
if cap(*params) < int(globalParamsCount) {
|
||||
newParams := make(Params, len(*params), globalParamsCount)
|
||||
copy(newParams, *params)
|
||||
*params = newParams
|
||||
}
|
||||
|
||||
if value.params == nil {
|
||||
value.params = params
|
||||
}
|
||||
|
@ -526,12 +536,12 @@ walk: // Outer loop for walking the tree
|
|||
|
||||
// ... but we can't
|
||||
value.tsr = len(path) == end+1
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
return value
|
||||
}
|
||||
if len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
|
@ -539,11 +549,18 @@ walk: // Outer loop for walking the tree
|
|||
n = n.children[0]
|
||||
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
|
||||
}
|
||||
return
|
||||
return value
|
||||
|
||||
case catchAll:
|
||||
// Save param value
|
||||
if params != nil {
|
||||
// Preallocate capacity if necessary
|
||||
if cap(*params) < int(globalParamsCount) {
|
||||
newParams := make(Params, len(*params), globalParamsCount)
|
||||
copy(newParams, *params)
|
||||
*params = newParams
|
||||
}
|
||||
|
||||
if value.params == nil {
|
||||
value.params = params
|
||||
}
|
||||
|
@ -564,7 +581,7 @@ walk: // Outer loop for walking the tree
|
|||
|
||||
value.handlers = n.handlers
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
return value
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
|
@ -595,7 +612,7 @@ walk: // Outer loop for walking the tree
|
|||
// Check if this node has a handle registered.
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// If there is no handle for this route, but this route has a
|
||||
|
@ -603,12 +620,12 @@ walk: // Outer loop for walking the tree
|
|||
// additional trailing slash
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
value.tsr = true
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
if path == "/" && n.nType == static {
|
||||
value.tsr = true
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
|
@ -618,11 +635,11 @@ walk: // Outer loop for walking the tree
|
|||
n = n.children[i]
|
||||
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||
return
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return value
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
|
@ -648,7 +665,7 @@ walk: // Outer loop for walking the tree
|
|||
}
|
||||
}
|
||||
|
||||
return
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
36
tree_test.go
36
tree_test.go
|
@ -417,6 +417,8 @@ func TestTreeWildcardConflict(t *testing.T) {
|
|||
{"/user_:name", false},
|
||||
{"/id:id", false},
|
||||
{"/id/:id", false},
|
||||
{"/static/*file", false},
|
||||
{"/static/", true},
|
||||
}
|
||||
testRoutes(t, routes)
|
||||
}
|
||||
|
@ -893,9 +895,9 @@ func TestTreeInvalidNodeType(t *testing.T) {
|
|||
|
||||
func TestTreeInvalidParamsType(t *testing.T) {
|
||||
tree := &node{}
|
||||
tree.wildChild = true
|
||||
tree.children = append(tree.children, &node{})
|
||||
tree.children[0].nType = 2
|
||||
// add a child with wildcard
|
||||
route := "/:path"
|
||||
tree.addRoute(route, fakeHandler(route))
|
||||
|
||||
// set invalid Params type
|
||||
params := make(Params, 0)
|
||||
|
@ -904,6 +906,34 @@ func TestTreeInvalidParamsType(t *testing.T) {
|
|||
tree.getValue("/test", ¶ms, getSkippedNodes(), false)
|
||||
}
|
||||
|
||||
func TestTreeExpandParamsCapacity(t *testing.T) {
|
||||
data := []struct {
|
||||
path string
|
||||
}{
|
||||
{"/:path"},
|
||||
{"/*path"},
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
tree := &node{}
|
||||
tree.addRoute(item.path, fakeHandler(item.path))
|
||||
params := make(Params, 0)
|
||||
|
||||
value := tree.getValue("/test", ¶ms, getSkippedNodes(), false)
|
||||
|
||||
if value.params == nil {
|
||||
t.Errorf("Expected %s params to be set, but they weren't", item.path)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(*value.params) != 1 {
|
||||
t.Errorf("Wrong number of %s params: got %d, want %d",
|
||||
item.path, len(*value.params), 1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeWildcardConflictEx(t *testing.T) {
|
||||
conflicts := [...]struct {
|
||||
route string
|
||||
|
|
2
utils.go
2
utils.go
|
@ -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.
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
package gin
|
||||
|
||||
// Version is the current gin framework's version.
|
||||
const Version = "v1.8.2"
|
||||
const Version = "v1.9.1"
|
||||
|
|
Loading…
Reference in New Issue