Merge branch 'master' into patch-1

This commit is contained in:
Jose Diaz-Gonzalez 2023-06-03 13:29:58 -04:00 committed by GitHub
commit 1362981899
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 8309 additions and 4345 deletions

View File

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

View File

@ -1,7 +1,7 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integration systems such as TravisCI.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly

49
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,49 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '0 17 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
strategy:
fail-fast: false
matrix:
# 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']
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

77
.github/workflows/gin.yml vendored Normal file
View File

@ -0,0 +1,77 @@
name: Run Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v4
with:
go-version: '^1.18'
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v3.4.0
with:
version: v1.52.2
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ['1.18', '1.19', '1.20']
test-tags: ['', '-tags nomsgpack', '-tags "sonic avx"', '-tags go_json']
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
- os: macos-latest
go-build: ~/Library/Caches/go-build
name: ${{ matrix.os }} @ Go ${{ matrix.go }} ${{ matrix.test-tags }}
runs-on: ${{ matrix.os }}
env:
GO111MODULE: on
TESTTAGS: ${{ matrix.test-tags }}
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
- name: Checkout Code
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- uses: actions/cache@v3
with:
path: |
${{ matrix.go-build }}
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run Tests
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
- name: Format
if: matrix.go-version == '1.20.x'
run: diff -u <(echo -n) <(gofmt -d .)

34
.github/workflows/goreleaser.yml vendored Normal file
View File

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

58
.golangci.yml Normal file
View File

@ -0,0 +1,58 @@
run:
timeout: 5m
linters:
enable:
- asciicheck
- depguard
- dogsled
- durationcheck
- errcheck
- errorlint
- exportloopref
- gci
- gofmt
- goimports
- gosec
- misspell
- nakedret
- nilerr
- 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:
- structcheck
- unused
text: "`data` is unused"
- linters:
- staticcheck
text: "SA1019:"
- linters:
- revive
text: "var-naming:"
- linters:
- revive
text: "exported:"
- path: _test\.go
linters:
- gosec # security is not make sense in tests
- linters:
- revive
path: _test\.go

57
.goreleaser.yaml Normal file
View File

@ -0,0 +1,57 @@
project_name: gin
builds:
-
# If true, skip the build.
# Useful for library projects.
# Default is false
skip: true
changelog:
# Set it to true if you wish to skip the changelog generation.
# This may result in an empty release notes on GitHub/GitLab/Gitea.
skip: false
# Changelog generation implementation to use.
#
# Valid options are:
# - `git`: uses `git log`;
# - `github`: uses the compare GitHub API, appending the author login to the changelog.
# - `gitlab`: uses the compare GitLab API, appending the author name and email to the changelog.
# - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
#
# Defaults to `git`.
use: git
# Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty
# Default is empty
sort: asc
# Group commits messages by given regex and title.
# Order value defines the order of the groups.
# Proving no regex means all commits will be grouped under the default group.
# Groups are disabled when using github-native, as it already groups things by itself.
#
# Default is no groups.
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: 'Enhancements'
regexp: "^.*chore[(\\w)]*:+.*$"
order: 2
- title: Others
order: 999
filters:
# Commit messages matching the regexp listed here will be removed from
# the changelog
# Default is empty
exclude:
- '^docs'
- 'CICD'
- typo

View File

@ -1,48 +0,0 @@
language: go
matrix:
fast_finish: true
include:
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: 1.13.x
- go: 1.13.x
env:
- TESTTAGS=nomsgpack
- go: 1.14.x
- go: 1.14.x
env:
- TESTTAGS=nomsgpack
- go: master
git:
depth: 10
before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install:
- if [[ "${GO111MODULE}" = "on" ]]; then go mod download; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
go_import_path: github.com/gin-gonic/gin
script:
- make vet
- make fmt-check
- make misspell-check
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7f95bf605c4d356372f4
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false

View File

@ -2,230 +2,405 @@ List of all the awesome people working to make Gin the best Web Framework in Go.
## gin 1.x series authors
**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
**Gin Core Team:** Bo-Yi Wu (@appleboy), thinkerou (@thinkerou), Javier Provecho (@javierprovecho)
## gin 0.x series authors
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
------
People and companies, who have contributed, in alphabetical order.
**@858806258 (杰哥)**
- Fix typo in example
**@achedeuzot (Klemen Sever)**
- Fix newline debug printing
**@adammck (Adam Mckaig)**
- Add MIT license
**@AlexanderChen1989 (Alexander)**
- Typos in README
**@alexanderdidenko (Aleksandr Didenko)**
- Add support multipart/form-data
**@alexandernyquist (Alexander Nyquist)**
- Using template.Must to fix multiple return issue
- ★ Added support for OPTIONS verb
- ★ Setting response headers before calling WriteHeader
- Improved documentation for model binding
- ★ Added Content.Redirect()
- ★ Added tons of Unit tests
**@austinheap (Austin Heap)**
- Added travis CI integration
**@andredublin (Andre Dublin)**
- Fix typo in comment
**@bredov (Ludwig Valda Vasquez)**
- Fix html templating in debug mode
**@bluele (Jun Kimura)**
- Fixes code examples in README
**@chad-russell**
- ★ Support for serializing gin.H into XML
**@dickeyxxx (Jeff Dickey)**
- Typos in README
- Add example about serving static files
**@donileo (Adonis)**
- Add NoMethod handler
**@dutchcoders (DutchCoders)**
- ★ Fix security bug that allows client to spoof ip
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
**@el3ctro- (Joshua Loper)**
- Fix typo in example
**@ethankan (Ethan Kan)**
- Unsigned integers in binding
**(Evgeny Persienko)**
- Validate sub structures
**@frankbille (Frank Bille)**
- Add support for HTTP Realm Auth
**@fmd (Fareed Dudhia)**
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
**@ironiridis (Christopher Harrington)**
- Remove old reference
**@jammie-stackhouse (Jamie Stackhouse)**
- Add more shortcuts for router methods
**@jasonrhansen**
- Fix spelling and grammar errors in documentation
**@JasonSoft (Jason Lee)**
- Fix typo in comment
**@joiggama (Ignacio Galindo)**
- Add utf-8 charset header on renders
**@julienschmidt (Julien Schmidt)**
- gofmt the code examples
**@kelcecil (Kel Cecil)**
- Fix readme typo
**@kyledinh (Kyle Dinh)**
- Adds RunTLS()
**@LinusU (Linus Unnebäck)**
- Small fixes in README
**@loongmxbt (Saint Asky)**
- Fix typo in example
**@lucas-clemente (Lucas Clemente)**
- ★ work around path.Join removing trailing slashes from routes
**@mattn (Yasuhiro Matsumoto)**
- Improve color logger
**@mdigger (Dmitry Sedykh)**
- Fixes Form binding when content-type is x-www-form-urlencoded
- No repeat call c.Writer.Status() in gin.Logger
- Fixes Content-Type for json render
**@mirzac (Mirza Ceric)**
- Fix debug printing
**@mopemope (Yutaka Matsubara)**
- ★ Adds Godep support (Dependencies Manager)
- Fix variadic parameter in the flexible render API
- Fix Corrupted plain render
- Add Pluggable View Renderer Example
**@msemenistyi (Mykyta Semenistyi)**
- update Readme.md. Add code to String method
**@msoedov (Sasha Myasoedov)**
- ★ Adds tons of unit tests.
**@ngerakines (Nick Gerakines)**
- ★ Improves API, c.GET() doesn't panic
- Adds MustGet() method
**@r8k (Rajiv Kilaparti)**
- Fix Port usage in README.
**@rayrod2030 (Ray Rodriguez)**
- Fix typo in example
**@rns**
- Fix typo in example
**@RobAWilkinson (Robert Wilkinson)**
- Add example of forms and params
**@rogierlommers (Rogier Lommers)**
- Add updated static serve example
**@se77en (Damon Zhao)**
- Improve color logging
**@silasb (Silas Baronda)**
- Fixing quotes in README
**@SkuliOskarsson (Skuli Oskarsson)**
- Fixes some texts in README II
**@slimmy (Jimmy Pettersson)**
- Added messages for required bindings
**@smira (Andrey Smirnov)**
- Add support for ignored/unexported fields in binding
**@superalsrk (SRK.Lyu)**
- Update httprouter godeps
**@tebeka (Miki Tebeka)**
- Use net/http constants instead of numeric values
**@techjanitor**
- Update context.go reserved IPs
**@yosssi (Keiji Yoshida)**
- Fix link in README
**@yuyabee**
- Fixed README
- 178inaba <178inaba@users.noreply.github.com>
- A. F <hello@clivern.com>
- ABHISHEK SONI <abhishek.rocks26@gmail.com>
- Abhishek Chanda <achanda@users.noreply.github.com>
- Abner Chen <houjunchen@gmail.com>
- AcoNCodes <acongame@gmail.com>
- Adam Dratwinski <adam.dratwinski@gmail.com>
- Adam Mckaig <adam.mckaig@gmail.com>
- Adam Zielinski <MusicAdam@users.noreply.github.com>
- Adonis <donileo@gmail.com>
- Alan Wang <azzwacb9001@126.com>
- Albin Gilles <gilles.albin@gmail.com>
- Aleksandr Didenko <aa.didenko@yandex.ru>
- Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
- Alex <AWulkan@users.noreply.github.com>
- Alexander <alexanderchenmh@gmail.com>
- Alexander Lokhman <alex.lokhman@gmail.com>
- Alexander Melentyev <55826637+alexander-melentyev@users.noreply.github.com>
- Alexander Nyquist <nyquist.alexander@gmail.com>
- Allen Ren <kulong0105@gmail.com>
- AllinGo <tanhp@outlook.com>
- Ammar Bandukwala <ammar@ammar.io>
- An Xiao (Luffy) <hac@zju.edu.cn>
- Andre Dublin <81dublin@gmail.com>
- Andrew Szeto <github@jabagawee.com>
- Andrey Abramov <andreyabramov.aaa@gmail.com>
- Andrey Nering <andrey.nering@gmail.com>
- Andrey Smirnov <Smirnov.Andrey@gmail.com>
- Andrii Bubis <firstrow@gmail.com>
- André Bazaglia <bazaglia@users.noreply.github.com>
- Andy Pan <panjf2000@gmail.com>
- Antoine GIRARD <sapk@users.noreply.github.com>
- Anup Kumar Panwar <1anuppanwar@gmail.com>
- Aravinth Sundaram <gosh.aravind@gmail.com>
- Artem <horechek@gmail.com>
- Ashwani <ashwanisharma686@gmail.com>
- Aurelien Regat-Barrel <arb@cyberkarma.net>
- Austin Heap <me@austinheap.com>
- Barnabus <jbampton@users.noreply.github.com>
- Bo-Yi Wu <appleboy.tw@gmail.com>
- Boris Borshevsky <BorisBorshevsky@gmail.com>
- Boyi Wu <p581581@gmail.com>
- BradyBromley <51128276+BradyBromley@users.noreply.github.com>
- Brendan Fosberry <brendan@shopkeep.com>
- Brian Wigginton <brianwigginton@gmail.com>
- Carlos Eduardo <carlosedp@gmail.com>
- Chad Russell <chaddouglasrussell@gmail.com>
- Charles <cxjava@gmail.com>
- Christian Muehlhaeuser <muesli@gmail.com>
- Christian Persson <saser@live.se>
- Christopher Harrington <ironiridis@gmail.com>
- Damon Zhao <yijun.zhao@outlook.com>
- Dan Markham <dmarkham@gmail.com>
- Dang Nguyen <hoangdang.me@gmail.com>
- Daniel Krom <kromdan@gmail.com>
- Daniel M. Lambea <dmlambea@gmail.com>
- Danieliu <liudanking@gmail.com>
- David Irvine <aviddiviner@gmail.com>
- David Zhang <crispgm@gmail.com>
- Davor Kapsa <dvrkps@users.noreply.github.com>
- DeathKing <DeathKing@users.noreply.github.com>
- Dennis Cho <47404603+forest747@users.noreply.github.com>
- Dmitry Dorogin <dmirogin@ya.ru>
- Dmitry Kutakov <vkd.castle@gmail.com>
- Dmitry Sedykh <dmitrys@d3h.local>
- Don2Quixote <35610661+Don2Quixote@users.noreply.github.com>
- Donn Pebe <iam@donnpebe.com>
- Dustin Decker <dustindecker@protonmail.com>
- Eason Lin <easonlin404@gmail.com>
- Edward Betts <edward@4angle.com>
- Egor Seredin <4819888+agmt@users.noreply.github.com>
- Emmanuel Goh <emmanuel@visenze.com>
- Equim <sayaka@ekyu.moe>
- Eren A. Akyol <eren@redmc.me>
- Eric_Lee <xplzv@126.com>
- Erik Bender <erik.bender@develerik.dev>
- Ethan Kan <ethankan@neoplot.com>
- Evgeny Persienko <e.persienko@office.ngs.ru>
- Faisal Alam <ifaisalalam@gmail.com>
- Fareed Dudhia <fareeddudhia@googlemail.com>
- Filip Figiel <figiel.filip@gmail.com>
- Florian Polster <couchpolster@icqmail.com>
- Frank Bille <github@frankbille.dk>
- Franz Bettag <franz@bett.ag>
- Ganlv <ganlvtech@users.noreply.github.com>
- Gaozhen Ying <yinggaozhen@hotmail.com>
- George Gabolaev <gabolaev98@gmail.com>
- George Kirilenko <necryin@users.noreply.github.com>
- Georges Varouchas <georges.varouchas@gmail.com>
- Gordon Tyler <gordon@doxxx.net>
- Harindu Perera <harinduenator@gmail.com>
- Helios <674876158@qq.com>
- Henry Kwan <piengeng@users.noreply.github.com>
- Henry Yee <henry@yearning.io>
- Himanshu Mishra <OrkoHunter@users.noreply.github.com>
- Hiroyuki Tanaka <h.tanaka.0325@gmail.com>
- Ibraheem Ahmed <ibrah1440@gmail.com>
- Ignacio Galindo <joiggama@gmail.com>
- Igor H. Vieira <zignd.igor@gmail.com>
- Ildar1111 <54001462+Ildar1111@users.noreply.github.com>
- Iskander (Alex) Sharipov <iskander.sharipov@intel.com>
- Ismail Gjevori <isgjevori@protonmail.com>
- Ivan Chen <allenivan@gmail.com>
- JINNOUCHI Yasushi <delphinus@remora.cx>
- James Pettyjohn <japettyjohn@users.noreply.github.com>
- Jamie Stackhouse <jamie.stackhouse@redspace.com>
- Jason Lee <jawc@hotmail.com>
- Javier Provecho <j.provecho@dartekstudios.com>
- Javier Provecho <javier.provecho@bq.com>
- Javier Provecho <javiertitan@gmail.com>
- Javier Provecho Fernandez <j.provecho@dartekstudios.com>
- Javier Provecho Fernandez <javiertitan@gmail.com>
- Jean-Christophe Lebreton <jclebreton@gmail.com>
- Jeff <laojianzi1994@gmail.com>
- Jeremy Loy <jeremy.b.loy@icloud.com>
- Jim Filippou <p3160253@aueb.gr>
- Jimmy Pettersson <jimmy@expertmaker.com>
- John Bampton <jbampton@users.noreply.github.com>
- Johnny Dallas <johnnydallas0308@gmail.com>
- Johnny Dallas <theonlyjohnny@theonlyjohnny.sh>
- Jonathan (JC) Chen <jc@dijonkitchen.org>
- Josep Jesus Bigorra Algaba <42377845+averageflow@users.noreply.github.com>
- Josh Horowitz <joshua.m.horowitz@gmail.com>
- Joshua Loper <josh.el3@gmail.com>
- Julien Schmidt <github@julienschmidt.com>
- Jun Kimura <jksmphone@gmail.com>
- Justin Beckwith <justin.beckwith@gmail.com>
- Justin Israel <justinisrael@gmail.com>
- Justin Mayhew <mayhew@live.ca>
- Jérôme Laforge <jerome-laforge@users.noreply.github.com>
- Kacper Bąk <56700396+53jk1@users.noreply.github.com>
- Kamron Batman <kamronbatman@users.noreply.github.com>
- Kane Rogers <kane@cleanstream.com.au>
- Kaushik Neelichetty <kaushikneelichetty6132@gmail.com>
- Keiji Yoshida <yoshida.keiji.84@gmail.com>
- Kel Cecil <kel.cecil@listhub.com>
- Kevin Mulvey <kmulvey@linux.com>
- Kevin Zhu <ipandtcp@gmail.com>
- Kirill Motkov <motkov.kirill@gmail.com>
- Klemen Sever <ksever@student.42.fr>
- Kristoffer A. Iversen <kristoffer.a.iversen@gmail.com>
- Krzysztof Szafrański <k.p.szafranski@gmail.com>
- Kumar McMillan <kumar.mcmillan@gmail.com>
- Kyle Mcgill <email@kylescottmcgill.com>
- Lanco <35420416+lancoLiu@users.noreply.github.com>
- Levi Olson <olson.levi@gmail.com>
- Lin Kao-Yuan <mosdeo@gmail.com>
- Linus Unnebäck <linus@folkdatorn.se>
- Lucas Clemente <lucas@clemente.io>
- Ludwig Valda Vasquez <bredov@gmail.com>
- Luis GG <lggomez@users.noreply.github.com>
- MW Lim <williamchange@gmail.com>
- Maksimov Sergey <konjoot@gmail.com>
- Manjusaka <lizheao940510@gmail.com>
- Manu MA <manu.mtza@gmail.com>
- Manu MA <manu.valladolid@gmail.com>
- Manu Mtz-Almeida <manu.valladolid@gmail.com>
- Manu Mtz.-Almeida <manu.valladolid@gmail.com>
- Manuel Alonso <manuelalonso@invisionapp.com>
- Mara Kim <hacker.root@gmail.com>
- Mario Kostelac <mario@intercom.io>
- Martin Karlsch <martin@karlsch.com>
- Matt Newberry <mnewberry@opentable.com>
- Matt Williams <gh@mattyw.net>
- Matthieu MOREL <mmorel-35@users.noreply.github.com>
- Max Hilbrunner <mhilbrunner@users.noreply.github.com>
- Maxime Soulé <btik-git@scoubidou.com>
- MetalBreaker <johnymichelson@gmail.com>
- Michael Puncel <mpuncel@squareup.com>
- MichaelDeSteven <51652084+MichaelDeSteven@users.noreply.github.com>
- Mike <38686456+icy4ever@users.noreply.github.com>
- Mike Stipicevic <mst@ableton.com>
- Miki Tebeka <miki.tebeka@gmail.com>
- Miles <MilesLin@users.noreply.github.com>
- Mirza Ceric <mirza.ceric@b2match.com>
- Mykyta Semenistyi <nikeiwe@gmail.com>
- Naoki Takano <honten@tinkermode.com>
- Ngalim Siregar <ngalim.siregar@gmail.com>
- Ni Hao <supernihaooo@qq.com>
- Nick Gerakines <nick@gerakines.net>
- Nikifor Seryakov <nikandfor@gmail.com>
- Notealot <714804968@qq.com>
- Olivier Mengué <dolmen@cpan.org>
- Olivier Robardet <orobardet@users.noreply.github.com>
- Pablo Moncada <pablo.moncada@bq.com>
- Pablo Moncada <pmoncadaisla@gmail.com>
- Panmax <967168@qq.com>
- Peperoncino <2wua4nlyi@gmail.com>
- Philipp Meinen <philipp@bind.ch>
- Pierre Massat <pierre@massat.io>
- Qt <golang.chen@gmail.com>
- Quentin ROYER <aydendevg@gmail.com>
- README Bot <35302948+codetriage-readme-bot@users.noreply.github.com>
- Rafal Zajac <rzajac@gmail.com>
- Rahul Datta Roy <rahuldroy@users.noreply.github.com>
- Rajiv Kilaparti <rajivk085@gmail.com>
- Raphael Gavache <raphael.gavache@datadoghq.com>
- Ray Rodriguez <rayrod2030@gmail.com>
- Regner Blok-Andersen <shadowdf@gmail.com>
- Remco <remco@dutchcoders.io>
- Rex Lee(李俊) <duguying2008@gmail.com>
- Richard Lee <dlackty@gmail.com>
- Riverside <wangyb65@gmail.com>
- Robert Wilkinson <wilkinson.robert.a@gmail.com>
- Rogier Lommers <rogier@lommers.org>
- Rohan Pai <me@rohanpai.com>
- Romain Beuque <rbeuque74@gmail.com>
- Roman Belyakovsky <ihryamzik@gmail.com>
- Roman Zaynetdinov <627197+zaynetro@users.noreply.github.com>
- Roman Zaynetdinov <roman.zaynetdinov@lekane.com>
- Ronald Petty <ronald.petty@rx-m.com>
- Ross Wolf <31489089+rw-access@users.noreply.github.com>
- Roy Lou <roylou@gmail.com>
- Rubi <14269809+codenoid@users.noreply.github.com>
- Ryan <46182144+ryanker@users.noreply.github.com>
- Ryan J. Yoder <me@ryanjyoder.com>
- SRK.Lyu <superalsrk@gmail.com>
- Sai <sairoutine@gmail.com>
- Samuel Abreu <sdepaula@gmail.com>
- Santhosh Kumar <santhoshkumarr1096@gmail.com>
- Sasha Melentyev <sasha@melentyev.io>
- Sasha Myasoedov <msoedov@gmail.com>
- Segev Finer <segev208@gmail.com>
- Sergey Egorov <egorovhome@gmail.com>
- Sergey Fedchenko <seregayoga@bk.ru>
- Sergey Gonimar <sergey.gonimar@gmail.com>
- Sergey Ponomarev <me@sergey-ponomarev.ru>
- Serica <943914044@qq.com>
- Shamus Taylor <Shamus03@me.com>
- Shilin Wang <jarvisfironman@gmail.com>
- Shuo <openset.wang@gmail.com>
- Skuli Oskarsson <skuli@codeiak.io>
- Snawoot <vladislav-ex-github@vm-0.com>
- Sridhar Ratnakumar <srid@srid.ca>
- Steeve Chailloux <steeve@chaahk.com>
- Sudhir Mishra <sudhirxps@gmail.com>
- Suhas Karanth <sudo-suhas@users.noreply.github.com>
- TaeJun Park <miking38@gmail.com>
- Tatsuya Hoshino <tatsuya7.hoshino7@gmail.com>
- Tevic <tevic.tt@gmail.com>
- Tevin Jeffrey <tev.jeffrey@gmail.com>
- The Gitter Badger <badger@gitter.im>
- Thibault Jamet <tjamet@users.noreply.github.com>
- Thomas Boerger <thomas@webhippie.de>
- Thomas Schaffer <loopfz@gmail.com>
- Tommy Chu <tommychu2256@gmail.com>
- Tudor Roman <tudurom@gmail.com>
- Uwe Dauernheim <djui@users.noreply.github.com>
- Valentine Oragbakosi <valentine13400@gmail.com>
- Vas N <pnvasanth@users.noreply.github.com>
- Vasilyuk Vasiliy <By-Vasiliy@users.noreply.github.com>
- Victor Castell <victor@victorcastell.com>
- Vince Yuan <vince.yuan@gmail.com>
- Vyacheslav Dubinin <vyacheslav.dubinin@gmail.com>
- Waynerv <ampedee@gmail.com>
- Weilin Shi <934587911@qq.com>
- Xudong Cai <fifsky@gmail.com>
- Yasuhiro Matsumoto <mattn.jp@gmail.com>
- Yehezkiel Syamsuhadi <ybs@ybs.im>
- Yoshiki Nakagawa <yyoshiki41@gmail.com>
- Yoshiyuki Kinjo <yskkin+github@gmail.com>
- Yue Yang <g1enyy0ung@gmail.com>
- ZYunH <zyunhjob@163.com>
- Zach Newburgh <zach.newburgh@gmail.com>
- Zasda Yusuf Mikail <zasdaym@gmail.com>
- ZhangYunHao <zyunhjob@163.com>
- ZhiFeng Hu <hufeng1987@gmail.com>
- Zhu Xi <zhuxi910511@163.com>
- a2tt <usera2tt@gmail.com>
- ahuigo <1781999+ahuigo@users.noreply.github.com>
- ali <anio@users.noreply.github.com>
- aljun <salameryy@163.com>
- andrea <crypto.andrea@protonmail.ch>
- andriikushch <andrii.kushch@gmail.com>
- anoty <anjunyou@foxmail.com>
- awkj <hzzbiu@gmail.com>
- axiaoxin <254606826@qq.com>
- bbiao <bbbiao@gmail.com>
- bestgopher <84328409@qq.com>
- betahu <zhong.wenhuang@foxmail.com>
- bigwheel <k.bigwheel+eng@gmail.com>
- bn4t <17193640+bn4t@users.noreply.github.com>
- bullgare <bullgare@gmail.com>
- chainhelen <chainhelen@gmail.com>
- chenyang929 <chenyang929code@gmail.com>
- chriswhelix <chris.williams@helix.com>
- collinmsn <4130944@qq.com>
- cssivision <cssivision@gmail.com>
- danielalves <alves.lopes.dan@gmail.com>
- delphinus <delphinus@remora.cx>
- dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
- dickeyxxx <jeff@dickeyxxx.com>
- edebernis <emeric.debernis@gmail.com>
- error10 <error@ioerror.us>
- esplo <esplo@users.noreply.github.com>
- eudore <30709860+eudore@users.noreply.github.com>
- ffhelicopter <32922889+ffhelicopter@users.noreply.github.com>
- filikos <11477309+filikos@users.noreply.github.com>
- forging2012 <forging2012@users.noreply.github.com>
- goqihoo <goqihoo@gmail.com>
- grapeVine <treeui.old@gmail.com>
- guonaihong <guonaihong@qq.com>
- heige <daheige@users.noreply.github.com>
- heige <zhuwei313@hotmail.com>
- hellojukay <hellojukay@163.com>
- henrylee2cn <henrylee2cn@gmail.com>
- htobenothing <htobenothing@gmail.com>
- iamhesir <78344375+iamhesir@users.noreply.github.com>
- ijaa <kailiu2013@gmail.com>
- ishanray <ishan.iipm@gmail.com>
- ishanray <ishanray@users.noreply.github.com>
- itcloudy <272685110@qq.com>
- jarodsong6 <jarodsong6@gmail.com>
- jasonrhansen <jasonrodneyhansen@gmail.com>
- jincheng9 <perfume0607@gmail.com>
- joeADSP <75027008+joeADSP@users.noreply.github.com>
- junfengye <junfeng.yejf@gmail.com>
- kaiiak <aNxFi37X@outlook.com>
- kebo <kevinke2020@outlook.com>
- keke <19yamashita15@gmail.com>
- kishor kunal raj <68464660+kishorkunal-raj@users.noreply.github.com>
- kyledinh <kyledinh@gmail.com>
- lantw44 <lantw44@gmail.com>
- likakuli <1154584512@qq.com>
- linfangrong <linfangrong.liuxin@qq.com>
- linzi <873804682@qq.com>
- llgoer <yanghuxiao@vip.qq.com>
- long-road <13412081338@163.com>
- mbesancon <mathieu.besancon@gmail.com>
- mehdy <mehdy.khoshnoody@gmail.com>
- metal A-wing <freedom.awing.777@gmail.com>
- micanzhang <micanzhang@gmail.com>
- minarc <ragnhildmowinckel@gmail.com>
- mllu <mornlyn@gmail.com>
- mopemoepe <yutaka.matsubara@gmail.com>
- msoedov <msoedov@gmail.com>
- mstmdev <mstmdev@gmail.com>
- novaeye <fcoffee@gmail.com>
- olebedev <oolebedev@gmail.com>
- phithon <phith0n@users.noreply.github.com>
- pjgg <pablo.gonzalez.granados@gmail.com>
- qm012 <67568757+qm012@users.noreply.github.com>
- raymonder jin <rayjingithub@gmail.com>
- rns <ruslan.shvedov@gmail.com>
- root@andrea:~# <crypto.andrea@protonmail.ch>
- sekky0905 <20237968+sekky0905@users.noreply.github.com>
- senhtry <w169q169@gmail.com>
- shadrus <shadrus@gmail.com>
- silasb <silas.baronda@gmail.com>
- solos <lxl1217@gmail.com>
- songjiayang <songjiayang@users.noreply.github.com>
- sope <shenshouer@163.com>
- srt180 <30768686+srt180@users.noreply.github.com>
- stackerzzq <foo_stacker@yeah.net>
- sunshineplan <sunshineplan@users.noreply.github.com>
- syssam <s.y.s.sam.sys@gmail.com>
- techjanitor <puntme@gmail.com>
- techjanitor <techjanitor@users.noreply.github.com>
- thinkerou <thinkerou@gmail.com>
- thinkgo <49174849+thinkgos@users.noreply.github.com>
- tsirolnik <tsirolnik@users.noreply.github.com>
- tyltr <31768692+tylitianrui@users.noreply.github.com>
- vinhha96 <anhvinha1@gmail.com>
- voidman <retmain@foxmail.com>
- vz <vzvway@gmail.com>
- wei <wei840222@gmail.com>
- weibaohui <weibaohui@yeah.net>
- whirosan <whirosan@users.noreply.github.com>
- willnewrelic <will@newrelic.com>
- wssccc <wssccc@qq.com>
- wuhuizuo <wuhuizuo@126.com>
- xyb <xyb4638@gmail.com>
- y-yagi <yuuji.yaginuma@gmail.com>
- yiranzai <wuqingdzx@gmail.com>
- youzeliang <youzel@126.com>
- yugu <chenzilong_1227@foxmail.com>
- yuyabe <yuyabee@gmail.com>
- zebozhuang <zebozhuang@163.com>
- zero11-0203 <93071220+zero11-0203@users.noreply.github.com>
- zesani <7sin@outlook.co.th>
- zhanweidu <zhanweidu@163.com>
- zhing <zqwillseven@gmail.com>
- ziheng <zihenglv@gmail.com>
- zzjin <zzjin@users.noreply.github.com>
- 森 優太 <59682979+uta-mori@users.noreply.github.com>
- 杰哥 <858806258@qq.com>
- 涛叔 <hi@taoshu.in>
- 市民233 <mengrenxiong@gmail.com>
- 尹宝强 <wmdandme@gmail.com>
- 梦溪笔谈 <loongmxbt@gmail.com>
- 飞雪无情 <ls8707@gmail.com>
- 寻寻觅觅的Gopher <zoujh99@qq.com>

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,248 @@
# 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
### 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
* Fix the GO-2022-1144 vulnerability ([#3432]((https://github.com/gin-gonic/gin/pull/3432)))
## Gin v1.8.1
### ENHANCEMENTS
* feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172)
## Gin v1.8.0
### 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)
### 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)
* Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
### ENHANCEMENTS
* Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
* RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685)
* Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680)
* Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707)
* Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711)
* Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723)
* Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files)
* Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737)
* Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765)
* Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720)
* Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787)
* Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769)
* Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701)
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967)
* Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012)
* Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398)
* Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071)
* Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749)
* Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825)
* Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139)
* Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081)
* IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
### DOCS
* Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703)
## Gin v1.7.7
### 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).
* Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
* Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
### ENHANCEMENTS
* TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
* TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
### DOCS
* NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
## Gin v1.7.6
### BUG FIXES
* bump new release to fix v1.7.5 release error by using v1.7.4 codes.
## Gin v1.7.4
### BUG FIXES
* bump new release to fix checksum mismatch
## Gin v1.7.3
### 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
### 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
### 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
### 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))
* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
### ENHANCEMENTS
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
* remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
## Gin v1.6.3
### ENHANCEMENTS
* Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
## Gin v1.6.2
### BUFIXES
### 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
### BUFIXES
### 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 negotitation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* 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)
@ -41,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)
@ -54,7 +272,9 @@
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
### MISC
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)

View File

@ -8,6 +8,6 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integration systems such as TravisCI.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.

View File

@ -1,5 +1,6 @@
GO ?= go
GOFMT ?= gofmt "-s"
GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go")
@ -10,7 +11,7 @@ TESTTAGS ?= ""
test:
echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \
$(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
$(GO) test $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \
@ -67,5 +68,10 @@ misspell:
.PHONY: tools
tools:
go install golang.org/x/lint/golint; \
go install github.com/client9/misspell/cmd/misspell;
@if [ $(GO_VERSION) -gt 15 ]; then \
$(GO) install golang.org/x/lint/golint@latest; \
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
elif [ $(GO_VERSION) -lt 16 ]; then \
$(GO) install golang.org/x/lint/golint; \
$(GO) install github.com/client9/misspell/cmd/misspell; \
fi

2177
README.md

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
package gin
import (
"crypto/subtle"
"encoding/base64"
"net/http"
"strconv"
@ -30,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false
}
for _, pair := range a {
if pair.value == authValue {
if subtle.ConstantTimeCompare(bytesconv.StringToBytes(pair.value), bytesconv.StringToBytes(authValue)) == 1 {
return pair.user, true
}
}
@ -70,8 +71,9 @@ func BasicAuth(accounts Accounts) HandlerFunc {
}
func processAccounts(accounts Accounts) authPairs {
assert1(len(accounts) > 0, "Empty list of authorized credentials")
pairs := make(authPairs, 0, len(accounts))
length := len(accounts)
assert1(length > 0, "Empty list of authorized credentials")
pairs := make(authPairs, 0, length)
for user, password := range accounts {
assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password)

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
//go:build !nomsgpack
package binding
@ -21,6 +21,7 @@ const (
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
MIMETOML = "application/toml"
)
// Binding describes the interface which needs to be implemented for binding the
@ -28,42 +29,43 @@ const (
// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
Bind(*http.Request, any) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
BindBody([]byte, any) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
// but it reads the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
BindUri(map[string][]string, any) error
}
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2.
// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a slice|array, the validation should be performed travel on every element.
// If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(interface{}) error
ValidateStruct(any) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() interface{}
Engine() any
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
@ -81,6 +83,7 @@ var (
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
@ -101,6 +104,8 @@ func Default(method, contentType string) Binding {
return MsgPack
case MIMEYAML:
return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
@ -108,7 +113,7 @@ func Default(method, contentType string) Binding {
}
}
func validate(obj interface{}) error {
func validate(obj any) error {
if Validator == nil {
return nil
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
//go:build !nomsgpack
package binding

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build nomsgpack
//go:build nomsgpack
package binding
@ -19,6 +19,7 @@ const (
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
MIMETOML = "application/toml"
)
// Binding describes the interface which needs to be implemented for binding the
@ -26,42 +27,42 @@ const (
// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
Bind(*http.Request, any) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
BindBody([]byte, any) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
// but it reads the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
BindUri(map[string][]string, any) error
}
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2.
// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(interface{}) error
ValidateStruct(any) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() interface{}
Engine() any
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
@ -78,6 +79,7 @@ var (
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
@ -98,12 +100,14 @@ func Default(method, contentType string) Binding {
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
case MIMETOML:
return TOML
default: // case MIMEPOSTForm:
return Form
}
}
func validate(obj interface{}) error {
func validate(obj any) error {
if Validator == nil {
return nil
}

View File

@ -9,18 +9,18 @@ import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
type appkey struct {
@ -34,7 +34,7 @@ type QueryTest struct {
}
type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
}
type FooBarStruct struct {
@ -60,11 +60,11 @@ type FooDefaultBarStruct struct {
}
type FooStructUseNumber struct {
Foo interface{} `json:"foo" binding:"required"`
Foo any `json:"foo" binding:"required"`
}
type FooStructDisallowUnknownFields struct {
Foo interface{} `json:"foo" binding:"required"`
Foo any `json:"foo" binding:"required"`
}
type FooBarStructForTimeType struct {
@ -92,7 +92,7 @@ type FooStructForTimeTypeFailLocation struct {
}
type FooStructForMapType struct {
MapFoo map[string]interface{} `form:"map_foo"`
MapFoo map[string]any `form:"map_foo"`
}
type FooStructForIgnoreFormTag struct {
@ -105,7 +105,7 @@ type InvalidNameType struct {
type InvalidNameMapType struct {
TestName struct {
MapFoo map[string]interface{} `form:"map_foo"`
MapFoo map[string]any `form:"map_foo"`
}
}
@ -127,7 +127,7 @@ type FooStructForStructPointerType struct {
type FooStructForSliceMapType struct {
// Unknown type: not support map
SliceMapFoo []map[string]interface{} `form:"slice_map_foo"`
SliceMapFoo []map[string]any `form:"slice_map_foo"`
}
type FooStructForBoolType struct {
@ -140,7 +140,7 @@ type FooStructForStringPtrType struct {
}
type FooStructForMapPtrType struct {
PtrBar *map[string]interface{} `form:"ptr_bar"`
PtrBar *map[string]any `form:"ptr_bar"`
}
func TestBindingDefault(t *testing.T) {
@ -164,6 +164,9 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
assert.Equal(t, TOML, Default("POST", MIMETOML))
assert.Equal(t, TOML, Default("PUT", MIMETOML))
}
func TestBindingJSONNilBody(t *testing.T) {
@ -180,6 +183,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`)
}
func TestBindingJSONSlice(t *testing.T) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
}
func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t,
JSON, "json",
@ -200,6 +217,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}
func TestBindingJSONStringMap(t *testing.T) {
testBodyBindingStringMap(t, JSON,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
}
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
@ -336,6 +359,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer")
}
func TestBindingFormStringMap(t *testing.T) {
testBodyBindingStringMap(t, Form,
"/", "",
`foo=bar&hello=world`, "")
// Should pick the last value
testBodyBindingStringMap(t, Form,
"/", "",
`foo=something&foo=bar&hello=world`, "")
}
func TestBindingFormStringSliceMap(t *testing.T) {
obj := make(map[string][]string)
req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err := Form.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
target := map[string][]string{
"foo": {"something", "bar"},
"hello": {"world"},
}
assert.True(t, reflect.DeepEqual(obj, target))
objInvalid := make(map[string][]int)
req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err = Form.Bind(req, &objInvalid)
assert.Error(t, err)
}
func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/",
@ -366,6 +420,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "")
}
func TestBindingQueryStringMap(t *testing.T) {
b := Query
obj := make(map[string]string)
req := requestWithBody("GET", "/?foo=bar&hello=world", "")
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])
obj = make(map[string]string)
req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
err = b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "2", obj["foo"])
assert.Equal(t, "world", obj["hello"])
}
func TestBindingXML(t *testing.T) {
testBodyBinding(t,
XML, "xml",
@ -380,6 +456,20 @@ func TestBindingXMLFail(t *testing.T) {
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
}
func TestBindingTOML(t *testing.T) {
testBodyBinding(t,
TOML, "toml",
"/", "/",
`foo="bar"`, `bar="foo"`)
}
func TestBindingTOMLFail(t *testing.T) {
testBodyBindingFail(t,
TOML, "toml",
"/", "/",
`foo=\n"bar"`, `bar="foo"`)
}
func TestBindingYAML(t *testing.T) {
testBodyBinding(t,
YAML, "yaml",
@ -387,6 +477,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`)
}
func TestBindingYAMLStringMap(t *testing.T) {
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
testBodyBindingStringMap(t, YAML,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
}
func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
@ -558,12 +655,12 @@ func TestBindingFormFilesMultipart(t *testing.T) {
// file from os
f, _ := os.Open("form.go")
defer f.Close()
fileActual, _ := ioutil.ReadAll(f)
fileActual, _ := io.ReadAll(f)
// file from multipart
mf, _ := obj.File.Open()
defer mf.Close()
fileExpect, _ := ioutil.ReadAll(mf)
fileExpect, _ := io.ReadAll(mf)
assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
assert.Equal(t, obj.Foo, "bar")
@ -687,7 +784,7 @@ func TestHeaderBinding(t *testing.T) {
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
Fail map[string]interface{} `header:"fail"`
Fail map[string]any `header:"fail"`
}
err := h.Bind(req, &failStruct{})
@ -708,11 +805,11 @@ func TestUriBinding(t *testing.T) {
assert.Equal(t, "thinkerou", tag.Name)
type NotSupportStruct struct {
Name map[string]interface{} `uri:"name"`
Name map[string]any `uri:"name"`
}
var not NotSupportStruct
assert.Error(t, b.BindUri(m, &not))
assert.Equal(t, map[string]interface{}(nil), not.Name)
assert.Equal(t, map[string]any(nil), not.Name)
}
func TestUriInnerBinding(t *testing.T) {
@ -751,7 +848,6 @@ func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, ba
assert.Equal(t, 1, obj.Page)
assert.Equal(t, 2, obj.Size)
assert.Equal(t, "test-appkey", obj.Appkey)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
@ -1010,9 +1106,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t,
struct {
Idx int "form:\"idx\""
}(struct {
Idx int "form:\"idx\""
}{Idx: 123}),
}{Idx: 123},
obj.StructFoo)
case "StructPointer":
obj := FooStructForStructPointerType{}
@ -1021,9 +1115,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t,
struct {
Name string "form:\"name\""
}(struct {
Name string "form:\"name\""
}{Name: "thinkerou"}),
}{Name: "thinkerou"},
*obj.StructPointerFoo)
case "Map":
obj := FooStructForMapType{}
@ -1114,6 +1206,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err)
}
func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
var obj1 []FooStruct
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj1)
assert.NoError(t, err)
var obj2 []FooStruct
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj2)
assert.Error(t, err)
}
func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
obj := make(map[string]string)
req := requestWithBody("POST", path, body)
if b.Name() == "form" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])
if badPath != "" && badBody != "" {
obj = make(map[string]string)
req = requestWithBody("POST", badPath, badBody)
err = b.Bind(req, &obj)
assert.Error(t, err)
}
objInt := make(map[string]int)
req = requestWithBody("POST", path, body)
err = b.Bind(req, &objInt)
assert.Error(t, err)
}
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
@ -1214,11 +1346,18 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
obj := protoexample.Test{}
req := requestWithBody("POST", path, body)
req.Body = ioutil.NopCloser(&hook{})
req.Body = io.NopCloser(&hook{})
req.Header.Add("Content-Type", MIMEPROTOBUF)
err := b.Bind(req, &obj)
assert.Error(t, err)
invalidobj := FooStruct{}
req.Body = io.NopCloser(strings.NewReader(`{"msg":"hello"}`))
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = b.Bind(req, &invalidobj)
assert.Error(t, err)
assert.Equal(t, err.Error(), "obj is not ProtoMessage")
obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)

View File

@ -5,7 +5,9 @@
package binding
import (
"fmt"
"reflect"
"strings"
"sync"
"github.com/go-playground/validator/v10"
@ -16,29 +18,73 @@ type defaultValidator struct {
validate *validator.Validate
}
var _ StructValidator = &defaultValidator{}
type SliceValidationError []error
// Error concatenates all error elements in SliceValidationError into a single string separated by \n.
func (err SliceValidationError) Error() string {
n := len(err)
switch n {
case 0:
return ""
default:
var b strings.Builder
if err[0] != nil {
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
}
if n > 1 {
for i := 1; i < n; i++ {
if err[i] != nil {
b.WriteString("\n")
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
}
}
}
return b.String()
}
}
var _ StructValidator = (*defaultValidator)(nil)
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
value := reflect.ValueOf(obj)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
if valueType == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
return err
}
}
func (v *defaultValidator) ValidateStruct(obj any) error {
if obj == nil {
return nil
}
value := reflect.ValueOf(obj)
switch value.Kind() {
case reflect.Ptr:
return v.ValidateStruct(value.Elem().Interface())
case reflect.Struct:
return v.validateStruct(obj)
case reflect.Slice, reflect.Array:
count := value.Len()
validateRet := make(SliceValidationError, 0)
for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err)
}
}
if len(validateRet) == 0 {
return nil
}
return validateRet
default:
return nil
}
}
// validateStruct receives struct type
func (v *defaultValidator) validateStruct(obj any) error {
v.lazyinit()
return v.validate.Struct(obj)
}
// Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8
func (v *defaultValidator) Engine() interface{} {
// https://pkg.go.dev/github.com/go-playground/validator/v10
func (v *defaultValidator) Engine() any {
v.lazyinit()
return v.validate
}

View File

@ -0,0 +1,24 @@
// 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.
package binding
import (
"errors"
"strconv"
"testing"
)
func BenchmarkSliceValidationError(b *testing.B) {
const size int = 100
for i := 0; i < b.N; i++ {
e := make(SliceValidationError, size)
for j := 0; j < size; j++ {
e[j] = errors.New(strconv.Itoa(j))
}
if len(e.Error()) == 0 {
b.Errorf("error")
}
}
}

View File

@ -0,0 +1,88 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"testing"
)
func TestSliceValidationError(t *testing.T) {
tests := []struct {
name string
err SliceValidationError
want string
}{
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
{"has zero elements", SliceValidationError{}, ""},
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
{"has two elements",
SliceValidationError{
errors.New("first error"),
errors.New("second error"),
},
"[0]: first error\n[1]: second error",
},
{"has many elements",
SliceValidationError{
errors.New("first error"),
errors.New("second error"),
nil,
nil,
nil,
errors.New("last error"),
},
"[0]: first error\n[1]: second error\n[5]: last error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("SliceValidationError.Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestDefaultValidator(t *testing.T) {
type exampleStruct struct {
A string `binding:"max=8"`
B int `binding:"gt=0"`
}
tests := []struct {
name string
v *defaultValidator
obj any
wantErr bool
}{
{"validate nil obj", &defaultValidator{}, nil, false},
{"validate int obj", &defaultValidator{}, 3, false},
{"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
{"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
{"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
{"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
{"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
{"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
{"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
{"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
{"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
{"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
{"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
{"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
{"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
{"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
{"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
{"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
{"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
{"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -5,6 +5,7 @@
package binding
import (
"errors"
"net/http"
)
@ -18,15 +19,13 @@ func (formBinding) Name() string {
return "form"
}
func (formBinding) Bind(req *http.Request, obj interface{}) error {
func (formBinding) Bind(req *http.Request, obj any) error {
if err := req.ParseForm(); err != nil {
return err
}
if err := req.ParseMultipartForm(defaultMemory); err != nil {
if err != http.ErrNotMultipart {
if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
return err
}
}
if err := mapForm(obj, req.Form); err != nil {
return err
}
@ -37,7 +36,7 @@ func (formPostBinding) Name() string {
return "form-urlencoded"
}
func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
func (formPostBinding) Bind(req *http.Request, obj any) error {
if err := req.ParseForm(); err != nil {
return err
}
@ -51,7 +50,7 @@ func (formMultipartBinding) Name() string {
return "multipart/form-data"
}
func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
func (formMultipartBinding) Bind(req *http.Request, obj any) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err
}

View File

@ -16,25 +16,52 @@ import (
"github.com/gin-gonic/gin/internal/json"
)
var errUnknownType = errors.New("unknown type")
var (
errUnknownType = errors.New("unknown type")
func mapUri(ptr interface{}, m map[string][]string) error {
// ErrConvertMapStringSlice can not convert to map[string][]string
ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
// ErrConvertToMapString can not convert to map[string]string
ErrConvertToMapString = errors.New("can not convert to map of strings")
)
func mapURI(ptr any, m map[string][]string) error {
return mapFormByTag(ptr, m, "uri")
}
func mapForm(ptr interface{}, form map[string][]string) error {
func mapForm(ptr any, form map[string][]string) error {
return mapFormByTag(ptr, form, "form")
}
func MapFormWithTag(ptr any, form map[string][]string, tag string) error {
return mapFormByTag(ptr, form, tag)
}
var emptyField = reflect.StructField{}
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
func mapFormByTag(ptr any, form map[string][]string, tag string) error {
// Check if ptr is a map
ptrVal := reflect.ValueOf(ptr)
var pointed any
if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem()
pointed = ptrVal.Interface()
}
if ptrVal.Kind() == reflect.Map &&
ptrVal.Type().Key().Kind() == reflect.String {
if pointed != nil {
ptr = pointed
}
return setFormMap(ptr, form)
}
return mappingByPtr(ptr, formSource(form), tag)
}
// setter tries to set value on a walking by fields of a struct
type setter interface {
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
}
type formSource map[string][]string
@ -42,11 +69,11 @@ type formSource map[string][]string
var _ setter = formSource(nil)
// TrySet tries to set a value by request's form source (like map[string][]string)
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {
return setByForm(value, field, form, tagValue, opt)
}
func mappingByPtr(ptr interface{}, setter setter, tag string) error {
func mappingByPtr(ptr any, setter setter, tag string) error {
_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
return err
}
@ -56,7 +83,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
return false, nil
}
var vKind = value.Kind()
vKind := value.Kind()
if vKind == reflect.Ptr {
var isNew bool
@ -65,14 +92,14 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
isNew = true
vPtr = reflect.New(value.Type().Elem())
}
isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
isSet, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil {
return false, err
}
if isNew && isSetted {
if isNew && isSet {
value.Set(vPtr)
}
return isSetted, nil
return isSet, nil
}
if vKind != reflect.Struct || !field.Anonymous {
@ -88,19 +115,19 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
if vKind == reflect.Struct {
tValue := value.Type()
var isSetted bool
var isSet bool
for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil {
return false, err
}
isSetted = isSetted || ok
isSet = isSet || ok
}
return isSetted, nil
return isSet, nil
}
return false, nil
}
@ -137,7 +164,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
return setter.TrySet(value, field, tagValue, setOpt)
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
return false, nil
@ -183,7 +210,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case reflect.Int64:
switch value.Interface().(type) {
case time.Duration:
return setTimeDuration(val, value, field)
return setTimeDuration(val, value)
}
return setIntField(val, 64, value)
case reflect.Uint:
@ -270,7 +297,7 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 0)
tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
@ -283,7 +310,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" {
@ -333,7 +359,7 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
return nil
}
func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
func setTimeDuration(val string, value reflect.Value) error {
d, err := time.ParseDuration(val)
if err != nil {
return err
@ -349,3 +375,29 @@ func head(str, sep string) (head string, tail string) {
}
return str[:idx], str[idx+len(sep):]
}
func setFormMap(ptr any, form map[string][]string) error {
el := reflect.TypeOf(ptr).Elem()
if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string)
if !ok {
return ErrConvertMapStringSlice
}
for k, v := range form {
ptrMap[k] = v
}
return nil
}
ptrMap, ok := ptr.(map[string]string)
if !ok {
return ErrConvertToMapString
}
for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last
}
return nil
}

View File

@ -18,9 +18,9 @@ func TestMappingBaseTypes(t *testing.T) {
}
for _, tt := range []struct {
name string
value interface{}
value any
form string
expect interface{}
expect any
}{
{"base type", struct{ F int }{}, "9", int(9)},
{"base type", struct{ F int8 }{}, "9", int8(9)},
@ -114,7 +114,7 @@ func TestMappingPrivateField(t *testing.T) {
}
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
assert.NoError(t, err)
assert.Equal(t, int(0), s.f)
assert.Equal(t, 0, s.f)
}
func TestMappingUnknownFieldType(t *testing.T) {
@ -131,9 +131,9 @@ func TestMappingURI(t *testing.T) {
var s struct {
F int `uri:"field"`
}
err := mapUri(&s, map[string][]string{"field": {"6"}})
err := mapURI(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
assert.Equal(t, 6, s.F)
}
func TestMappingForm(t *testing.T) {
@ -142,7 +142,16 @@ func TestMappingForm(t *testing.T) {
}
err := mapForm(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
assert.Equal(t, 6, s.F)
}
func TestMapFormWithTag(t *testing.T) {
var s struct {
F int `externalTag:"field"`
}
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
assert.NoError(t, err)
assert.Equal(t, 6, s.F)
}
func TestMappingTime(t *testing.T) {
@ -190,7 +199,7 @@ func TestMappingTime(t *testing.T) {
assert.Error(t, err)
}
func TestMapiingTimeDuration(t *testing.T) {
func TestMappingTimeDuration(t *testing.T) {
var s struct {
D time.Duration
}

View File

@ -1,3 +1,7 @@
// 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.
package binding
import (
@ -12,7 +16,7 @@ func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
func (headerBinding) Bind(req *http.Request, obj any) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
@ -21,7 +25,7 @@ func (headerBinding) Bind(req *http.Request, obj interface{}) error {
return validate(obj)
}
func mapHeader(ptr interface{}, h map[string][]string) error {
func mapHeader(ptr any, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}
@ -29,6 +33,6 @@ type headerSource map[string][]string
var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (bool, error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}

View File

@ -6,7 +6,7 @@ package binding
import (
"bytes"
"fmt"
"errors"
"io"
"net/http"
@ -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
@ -30,18 +30,18 @@ func (jsonBinding) Name() string {
return "json"
}
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
func (jsonBinding) Bind(req *http.Request, obj any) error {
if req == nil || req.Body == nil {
return fmt.Errorf("invalid request")
return errors.New("invalid request")
}
return decodeJSON(req.Body, obj)
}
func (jsonBinding) BindBody(body []byte, obj interface{}) error {
func (jsonBinding) BindBody(body []byte, obj any) error {
return decodeJSON(bytes.NewReader(body), obj)
}
func decodeJSON(r io.Reader, obj interface{}) error {
func decodeJSON(r io.Reader, obj any) error {
decoder := json.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()

View File

@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func TestJSONBindingBindBodyMap(t *testing.T) {
s := make(map[string]string)
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
require.NoError(t, err)
assert.Len(t, s, 2)
assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"])
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
//go:build !nomsgpack
package binding
@ -20,15 +20,15 @@ func (msgpackBinding) Name() string {
return "msgpack"
}
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
func (msgpackBinding) Bind(req *http.Request, obj any) error {
return decodeMsgPack(req.Body, obj)
}
func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
func (msgpackBinding) BindBody(body []byte, obj any) error {
return decodeMsgPack(bytes.NewReader(body), obj)
}
func decodeMsgPack(r io.Reader, obj interface{}) error {
func decodeMsgPack(r io.Reader, obj any) error {
cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
//go:build !nomsgpack
package binding
@ -25,7 +25,7 @@ func TestMsgpackBindingBindBody(t *testing.T) {
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj interface{}) []byte {
func msgpackBody(t *testing.T, obj any) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)

View File

@ -15,8 +15,16 @@ type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
// ErrMultiFileHeader multipart.FileHeader invalid
ErrMultiFileHeader = errors.New("unsupported field type for multipart.FileHeader")
// ErrMultiFileHeaderLenInvalid array for []*multipart.FileHeader len invalid
ErrMultiFileHeaderLenInvalid = errors.New("unsupported len of array for []*multipart.FileHeader")
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (bool, error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
@ -24,7 +32,7 @@ func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
@ -40,26 +48,26 @@ func setByMultipartFormFile(value reflect.Value, field reflect.StructField, file
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
isSet, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSet {
return isSet, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, errors.New("unsupported field type for multipart.FileHeader")
return false, ErrMultiFileHeader
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
return false, ErrMultiFileHeaderLenInvalid
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
set, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !set {
return set, err
}
}
return true, nil

View File

@ -6,7 +6,7 @@ package binding
import (
"bytes"
"io/ioutil"
"io"
"mime/multipart"
"net/http"
"testing"
@ -76,7 +76,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
for _, tt := range []struct {
name string
s interface{}
s any
}{
{"wrong type", &struct {
Files int `form:"file"`
@ -124,12 +124,12 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open()
assert.NoError(t, err)
body, err := ioutil.ReadAll(fl)
body, err := io.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))

View File

@ -5,10 +5,11 @@
package binding
import (
"io/ioutil"
"errors"
"io"
"net/http"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
)
type protobufBinding struct{}
@ -17,16 +18,20 @@ func (protobufBinding) Name() string {
return "protobuf"
}
func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
buf, err := ioutil.ReadAll(req.Body)
func (b protobufBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err != nil {
return err
}
return b.BindBody(buf, obj)
}
func (protobufBinding) BindBody(body []byte, obj interface{}) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
func (protobufBinding) BindBody(body []byte, obj any) error {
msg, ok := obj.(proto.Message)
if !ok {
return errors.New("obj is not ProtoMessage")
}
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but util now we can't add

View File

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

35
binding/toml.go Normal file
View File

@ -0,0 +1,35 @@
// 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.
package binding
import (
"bytes"
"io"
"net/http"
"github.com/pelletier/go-toml/v2"
)
type tomlBinding struct{}
func (tomlBinding) Name() string {
return "toml"
}
func (tomlBinding) Bind(req *http.Request, obj any) error {
return decodeToml(req.Body, obj)
}
func (tomlBinding) BindBody(body []byte, obj any) error {
return decodeToml(bytes.NewReader(body), obj)
}
func decodeToml(r io.Reader, obj any) error {
decoder := toml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return decoder.Decode(obj)
}

22
binding/toml_test.go Normal file
View File

@ -0,0 +1,22 @@
// 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.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTOMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `toml:"foo"`
}
tomlBody := `foo="FOO"`
err := tomlBinding{}.BindBody([]byte(tomlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -10,8 +10,8 @@ func (uriBinding) Name() string {
return "uri"
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
if err := mapUri(obj, m); err != nil {
func (uriBinding) BindUri(m map[string][]string, obj any) error {
if err := mapURI(obj, m); err != nil {
return err
}
return validate(obj)

View File

@ -59,7 +59,7 @@ type structNoValidationValues struct {
StructSlice []substructNoValidation
InterfaceSlice []testInterface
UniversalInterface interface{}
UniversalInterface any
CustomInterface testInterface
FloatMap map[string]float32
@ -169,7 +169,7 @@ func TestValidateNoValidationPointers(t *testing.T) {
//assert.Equal(t, origin, test)
}
type Object map[string]interface{}
type Object map[string]any
func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1}

View File

@ -17,14 +17,14 @@ func (xmlBinding) Name() string {
return "xml"
}
func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
func (xmlBinding) Bind(req *http.Request, obj any) error {
return decodeXML(req.Body, obj)
}
func (xmlBinding) BindBody(body []byte, obj interface{}) error {
func (xmlBinding) BindBody(body []byte, obj any) error {
return decodeXML(bytes.NewReader(body), obj)
}
func decodeXML(r io.Reader, obj interface{}) error {
func decodeXML(r io.Reader, obj any) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err

View File

@ -9,7 +9,7 @@ import (
"io"
"net/http"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
type yamlBinding struct{}
@ -18,15 +18,15 @@ func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
func (yamlBinding) Bind(req *http.Request, obj any) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
func (yamlBinding) BindBody(body []byte, obj any) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj interface{}) error {
func decodeYAML(r io.Reader, obj any) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err

View File

@ -1,5 +0,0 @@
coverage:
notify:
gitter:
default:
url: https://webhooks.gitter.im/e/d90dcdeeab2f1e357165

View File

@ -6,15 +6,15 @@ package gin
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
@ -34,10 +34,17 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
MIMETOML = binding.MIMETOML
)
const abortIndex int8 = math.MaxInt8 / 2
// BodyBytesKey indicates a default body bytes key.
const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
// ContextKey is the key that a Context returns itself for.
const ContextKey = "_gin-gonic/gin/contextkey"
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
@ -52,12 +59,14 @@ type Context struct {
fullPath string
engine *Engine
params *Params
skippedNodes *[]skippedNode
// This mutex protect Keys map
KeysMutex *sync.RWMutex
// This mutex protects Keys map.
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
Keys map[string]any
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
@ -65,10 +74,10 @@ type Context struct {
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
// queryCache caches the query result from c.Request.URL.Query().
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
@ -83,27 +92,35 @@ type Context struct {
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.Params = c.Params[:0]
c.handlers = nil
c.index = -1
c.KeysMutex = &sync.RWMutex{}
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Errors = c.Errors[:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
c.sameSite = 0
*c.params = (*c.params)[:0]
*c.skippedNodes = (*c.skippedNodes)[:0]
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
var cp = *c
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]interface{}{}
cp.Keys = map[string]any{}
for k, v := range c.Keys {
cp.Keys[k] = v
}
@ -136,6 +153,7 @@ func (c *Context) Handler() HandlerFunc {
// FullPath returns a matched route full path. For not found routes
// returns an empty string.
//
// router.GET("/user/:id", func(c *gin.Context) {
// c.FullPath() == "/user/:id" // true
// })
@ -182,7 +200,7 @@ func (c *Context) AbortWithStatus(code int) {
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code and return a JSON body.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
func (c *Context) AbortWithStatusJSON(code int, jsonObj any) {
c.Abort()
c.JSON(code, jsonObj)
}
@ -209,7 +227,8 @@ func (c *Context) Error(err error) *Error {
panic("err is nil")
}
parsedError, ok := err.(*Error)
var parsedError *Error
ok := errors.As(err, &parsedError)
if !ok {
parsedError = &Error{
Err: err,
@ -227,35 +246,27 @@ func (c *Context) Error(err error) *Error {
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {
if c.KeysMutex == nil {
c.KeysMutex = &sync.RWMutex{}
}
c.KeysMutex.Lock()
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]interface{})
c.Keys = make(map[string]any)
}
c.Keys[key] = value
c.KeysMutex.Unlock()
}
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
if c.KeysMutex == nil {
c.KeysMutex = &sync.RWMutex{}
}
c.KeysMutex.RLock()
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
c.KeysMutex.RUnlock()
return
}
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) interface{} {
func (c *Context) MustGet(key string) any {
if value, exists := c.Get(key); exists {
return value
}
@ -294,6 +305,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
return
}
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) {
if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
}
// GetUint64 returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint64(key string) (ui64 uint64) {
if val, ok := c.Get(key); ok && val != nil {
ui64, _ = val.(uint64)
}
return
}
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
@ -327,9 +354,9 @@ func (c *Context) GetStringSlice(key string) (ss []string) {
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]interface{}) {
func (c *Context) GetStringMap(key string) (sm map[string]any) {
if val, ok := c.Get(key); ok && val != nil {
sm, _ = val.(map[string]interface{})
sm, _ = val.(map[string]any)
}
return
}
@ -356,30 +383,44 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
//
// 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/"
// })
func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
// AddParam adds param to context and
// replaces path param key with given value for e2e testing purposes
// Example Route: "/user/:id"
// AddParam("id", 1)
// Result: "/user/1"
func (c *Context) AddParam(key, value string) {
c.Params = append(c.Params, Param{Key: key, Value: value})
}
// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//
// GET /path?id=1234&name=Manu&value=
// c.Query("id") == "1234"
// c.Query("name") == "Manu"
// c.Query("value") == ""
// c.Query("wtf") == ""
func (c *Context) Query(key string) string {
value, _ := c.GetQuery(key)
return value
func (c *Context) Query(key string) (value string) {
value, _ = c.GetQuery(key)
return
}
// DefaultQuery returns the keyed url query value if it exists,
// otherwise it returns the specified defaultValue string.
// See: Query() and GetQuery() for further information.
//
// GET /?name=Manu&lastname=
// c.DefaultQuery("name", "unknown") == "Manu"
// c.DefaultQuery("id", "none") == "none"
@ -395,6 +436,7 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
// if it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns `("", false)`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//
// GET /?name=Manu&lastname=
// ("Manu", true) == c.GetQuery("name")
// ("", false) == c.GetQuery("id")
@ -408,45 +450,47 @@ func (c *Context) GetQuery(key string) (string, bool) {
// QueryArray returns a slice of strings for a given query key.
// The length of the slice depends on the number of params with the given key.
func (c *Context) QueryArray(key string) []string {
values, _ := c.GetQueryArray(key)
return values
func (c *Context) QueryArray(key string) (values []string) {
values, _ = c.GetQueryArray(key)
return
}
func (c *Context) getQueryCache() {
func (c *Context) initQueryCache() {
if c.queryCache == nil {
if c.Request != nil {
c.queryCache = c.Request.URL.Query()
} else {
c.queryCache = url.Values{}
}
}
}
// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
c.getQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true
}
return []string{}, false
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
c.initQueryCache()
values, ok = c.queryCache[key]
return
}
// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
dicts, _ := c.GetQueryMap(key)
return dicts
func (c *Context) QueryMap(key string) (dicts map[string]string) {
dicts, _ = c.GetQueryMap(key)
return
}
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
c.getQueryCache()
c.initQueryCache()
return c.get(c.queryCache, key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string {
value, _ := c.GetPostForm(key)
return value
func (c *Context) PostForm(key string) (value string) {
value, _ = c.GetPostForm(key)
return
}
// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form
@ -463,6 +507,7 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
// form or multipart form when it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns ("", false).
// For example, during a PATCH request to update the user's email:
//
// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
// email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email
@ -475,17 +520,17 @@ func (c *Context) GetPostForm(key string) (string, bool) {
// PostFormArray returns a slice of strings for a given form key.
// The length of the slice depends on the number of params with the given key.
func (c *Context) PostFormArray(key string) []string {
values, _ := c.GetPostFormArray(key)
return values
func (c *Context) PostFormArray(key string) (values []string) {
values, _ = c.GetPostFormArray(key)
return
}
func (c *Context) getFormCache() {
func (c *Context) initFormCache() {
if c.formCache == nil {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
if !errors.Is(err, http.ErrNotMultipart) {
debugPrint("error on parse multipart form array: %v", err)
}
}
@ -495,28 +540,26 @@ func (c *Context) getFormCache() {
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
c.getFormCache()
if values := c.formCache[key]; len(values) > 0 {
return values, true
}
return []string{}, false
func (c *Context) GetPostFormArray(key string) (values []string, ok bool) {
c.initFormCache()
values, ok = c.formCache[key]
return
}
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
func (c *Context) PostFormMap(key string) (dicts map[string]string) {
dicts, _ = c.GetPostFormMap(key)
return
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
c.getFormCache()
c.initFormCache()
return c.get(c.formCache, key)
}
// get is an internal method and returns a map which satisfy conditions.
// get is an internal method and returns a map which satisfies conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
@ -560,6 +603,10 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
}
defer src.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
@ -570,49 +617,55 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
return err
}
// Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// Bind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example:
//
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// otherwise --> returns an error.
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj interface{}) error {
func (c *Context) Bind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b)
}
// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj interface{}) error {
func (c *Context) BindJSON(obj any) error {
return c.MustBindWith(obj, binding.JSON)
}
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj interface{}) error {
func (c *Context) BindXML(obj any) error {
return c.MustBindWith(obj, binding.XML)
}
// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).
func (c *Context) BindQuery(obj interface{}) error {
func (c *Context) BindQuery(obj any) error {
return c.MustBindWith(obj, binding.Query)
}
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
func (c *Context) BindYAML(obj interface{}) error {
func (c *Context) BindYAML(obj any) error {
return c.MustBindWith(obj, binding.YAML)
}
// BindTOML is a shortcut for c.MustBindWith(obj, binding.TOML).
func (c *Context) BindTOML(obj any) error {
return c.MustBindWith(obj, binding.TOML)
}
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj interface{}) error {
func (c *Context) BindHeader(obj any) error {
return c.MustBindWith(obj, binding.Header)
}
// BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
func (c *Context) BindUri(obj any) error {
if err := c.ShouldBindUri(obj); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
return err
}
return nil
@ -621,54 +674,60 @@ func (c *Context) BindUri(obj interface{}) error {
// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
return err
}
return nil
}
// ShouldBind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// ShouldBind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example:
//
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// otherwise --> returns an error
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid.
func (c *Context) ShouldBind(obj interface{}) error {
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
func (c *Context) ShouldBind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
func (c *Context) ShouldBindXML(obj any) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
func (c *Context) ShouldBindYAML(obj interface{}) error {
func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj interface{}) error {
func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
@ -678,7 +737,7 @@ func (c *Context) ShouldBindUri(obj interface{}) error {
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
@ -687,7 +746,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error) {
var body []byte
if cb, ok := c.Get(BodyBytesKey); ok {
if cbb, ok := cb.([]byte); ok {
@ -695,7 +754,7 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
}
}
if body == nil {
body, err = ioutil.ReadAll(c.Request.Body)
body, err = io.ReadAll(c.Request.Body)
if err != nil {
return err
}
@ -704,32 +763,55 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
return bb.BindBody(body, obj)
}
// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
// ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP {
clientIP := c.requestHeader("X-Forwarded-For")
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
}
if clientIP != "" {
return clientIP
// Check if we're running on a trusted platform, continue running backwards if error
if c.engine.TrustedPlatform != "" {
// Developers can define their own header of Trusted Platform or use predefined constants
if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
return addr
}
}
// Legacy "AppEngine" flag
if c.engine.AppEngine {
log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine' instead.`)
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined by Engine.SetTrustedProxies()
remoteIP := net.ParseIP(c.RemoteIP())
if remoteIP == nil {
return ""
}
trusted := c.engine.isTrustedProxy(remoteIP)
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
if valid {
return ip
}
}
}
return remoteIP.String()
}
// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
func (c *Context) RemoteIP() string {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil {
return ""
}
return ip
}
// ContentType returns the Content-Type header of the request.
@ -773,7 +855,7 @@ func (c *Context) Status(code int) {
c.Writer.WriteHeader(code)
}
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value).
// Header is an intelligent shortcut for c.Writer.Header().Set(key, value).
// It writes a header in the response.
// If value == "", this method removes the header `c.Writer.Header().Del(key)`
func (c *Context) Header(key, value string) {
@ -789,9 +871,9 @@ func (c *Context) GetHeader(key string) string {
return c.requestHeader(key)
}
// GetRawData return stream data.
// GetRawData returns stream data.
func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body)
return io.ReadAll(c.Request.Body)
}
// SetSameSite with cookie
@ -842,37 +924,39 @@ func (c *Context) Render(code int, r render.Render) {
}
if err := r.Render(c.Writer); err != nil {
panic(err)
// Pushing error to c.Errors
_ = c.Error(err)
c.Abort()
}
}
// HTML renders the HTTP template specified by its file name.
// It also updates the HTTP code and sets the Content-Type as "text/html".
// See http://golang.org/doc/articles/wiki/
func (c *Context) HTML(code int, name string, obj interface{}) {
func (c *Context) HTML(code int, name string, obj any) {
instance := c.engine.HTMLRender.Instance(name, obj)
c.Render(code, instance)
}
// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
// It also sets the Content-Type as "application/json".
// WARNING: we recommend to use this only for development purposes since printing pretty JSON is
// WARNING: we recommend using this only for development purposes since printing pretty JSON is
// more CPU and bandwidth consuming. Use Context.JSON() instead.
func (c *Context) IndentedJSON(code int, obj interface{}) {
func (c *Context) IndentedJSON(code int, obj any) {
c.Render(code, render.IndentedJSON{Data: obj})
}
// SecureJSON serializes the given struct as Secure JSON into the response body.
// Default prepends "while(1)," to response body if the given struct is array values.
// It also sets the Content-Type as "application/json".
func (c *Context) SecureJSON(code int, obj interface{}) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj})
func (c *Context) SecureJSON(code int, obj any) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})
}
// JSONP serializes the given struct as JSON into the response body.
// It add padding to response body to request data from a server residing in a different domain than the client.
// It adds padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) {
func (c *Context) JSONP(code int, obj any) {
callback := c.DefaultQuery("callback", "")
if callback == "" {
c.Render(code, render.JSON{Data: obj})
@ -883,44 +967,49 @@ func (c *Context) JSONP(code int, obj interface{}) {
// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
func (c *Context) JSON(code int, obj any) {
c.Render(code, render.JSON{Data: obj})
}
// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.
// It also sets the Content-Type as "application/json".
func (c *Context) AsciiJSON(code int, obj interface{}) {
func (c *Context) AsciiJSON(code int, obj any) {
c.Render(code, render.AsciiJSON{Data: obj})
}
// PureJSON serializes the given struct as JSON into the response body.
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
func (c *Context) PureJSON(code int, obj interface{}) {
func (c *Context) PureJSON(code int, obj any) {
c.Render(code, render.PureJSON{Data: obj})
}
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
func (c *Context) XML(code int, obj any) {
c.Render(code, render.XML{Data: obj})
}
// YAML serializes the given struct as YAML into the response body.
func (c *Context) YAML(code int, obj interface{}) {
func (c *Context) YAML(code int, obj any) {
c.Render(code, render.YAML{Data: obj})
}
// TOML serializes the given struct as TOML into the response body.
func (c *Context) TOML(code int, obj any) {
c.Render(code, render.TOML{Data: obj})
}
// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj interface{}) {
func (c *Context) ProtoBuf(code int, obj any) {
c.Render(code, render.ProtoBuf{Data: obj})
}
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) {
func (c *Context) String(code int, format string, values ...any) {
c.Render(code, render.String{Format: format, Data: values})
}
// Redirect returns a HTTP redirect to the specific location.
// Redirect returns an HTTP redirect to the specific location.
func (c *Context) Redirect(code int, location string) {
c.Render(-1, render.Redirect{
Code: code,
@ -947,12 +1036,12 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri
})
}
// File writes the specified file into the body stream in a efficient way.
// File writes the specified file into the body stream in an efficient way.
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way.
// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
defer func(old string) {
c.Request.URL.Path = old
@ -963,15 +1052,25 @@ 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) {
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
if isASCII(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))
}
http.ServeFile(c.Writer, c.Request, filepath)
}
// SSEvent writes a Server-Sent Event into the body stream.
func (c *Context) SSEvent(name string, message interface{}) {
func (c *Context) SSEvent(name string, message any) {
c.Render(-1, sse.Event{
Event: name,
Data: message,
@ -1005,14 +1104,15 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
type Negotiate struct {
Offered []string
HTMLName string
HTMLData interface{}
JSONData interface{}
XMLData interface{}
YAMLData interface{}
Data interface{}
HTMLData any
JSONData any
XMLData any
YAMLData any
Data any
TOMLData any
}
// Negotiate calls different Render according acceptable Accept format.
// Negotiate calls different Render according to acceptable Accept format.
func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) {
case binding.MIMEJSON:
@ -1031,8 +1131,12 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data)
case binding.MIMETOML:
data := chooseData(config.TOMLData, config.Data)
c.TOML(code, data)
default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
}
}
@ -1051,7 +1155,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune
i := 0
for ; i < len(accepted); i++ {
for ; i < len(accepted) && i < len(offer); i++ {
if accepted[i] == '*' || offer[i] == '*' {
return offer
}
@ -1076,34 +1180,54 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
// Deadline always returns that there is no deadline (ok==false),
// maybe you want to use Request.Context().Deadline() instead.
// 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.hasRequestContext() {
return
}
return c.Request.Context().Deadline()
}
// Done always returns nil (chan which will wait forever),
// if you want to abort your work when the connection was closed
// you should use Request.Context().Done() instead.
// Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} {
if !c.hasRequestContext() {
return nil
}
return c.Request.Context().Done()
}
// Err always returns nil, maybe you want to use Request.Context().Err() instead.
// Err returns nil when c.Request has no Context.
func (c *Context) Err() error {
if !c.hasRequestContext() {
return nil
}
return c.Request.Context().Err()
}
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
func (c *Context) Value(key interface{}) interface{} {
func (c *Context) Value(key any) any {
if key == 0 {
return c.Request
}
if key == ContextKey {
return c
}
if keyAsString, ok := key.(string); ok {
val, _ := c.Get(keyAsString)
if val, exists := c.Get(keyAsString); exists {
return val
}
}
if !c.hasRequestContext() {
return nil
}
return c.Request.Context().Value(key)
}

37
context_1.18_test.go Normal file
View File

@ -0,0 +1,37 @@
// 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.19
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed18(t *testing.T) {
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)
})
}

30
context_1.19_test.go Normal file
View File

@ -0,0 +1,30 @@
// 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.19
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed19(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}

View File

@ -1,11 +1,11 @@
// +build appengine
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build appengine
package gin
func init() {
defaultAppEngine = true
defaultPlatform = PlatformGoogleAppEngine
}

View File

@ -12,8 +12,10 @@ import (
"html/template"
"io"
"mime/multipart"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
@ -23,18 +25,19 @@ import (
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
var _ context.Context = &Context{}
var _ context.Context = (*Context)(nil)
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 {
@ -87,19 +90,6 @@ func TestContextFormFile(t *testing.T) {
assert.NoError(t, c.SaveUploadedFile(f, "test"))
}
func TestContextFormFileFailed(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
@ -158,13 +148,13 @@ func TestSaveUploadedCreateFailed(t *testing.T) {
func TestContextReset(t *testing.T) {
router := New()
c := router.allocateContext()
c := router.allocateContext(0)
assert.Equal(t, c.engine, router)
c.index = 2
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
c.Params = Params{Param{}}
c.Error(errors.New("test")) // nolint: errcheck
c.Error(errors.New("test")) //nolint: errcheck
c.Set("foo", "bar")
c.reset()
@ -224,7 +214,7 @@ func TestContextSetGetValues(t *testing.T) {
c.Set("uint64", uint64(42))
c.Set("float32", float32(4.2))
c.Set("float64", 4.2)
var a interface{} = 1
var a any = 1
c.Set("intInterface", a)
assert.Exactly(t, c.MustGet("string").(string), "this is a string")
@ -234,7 +224,6 @@ func TestContextSetGetValues(t *testing.T) {
assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2))
assert.Exactly(t, c.MustGet("float64").(float64), 4.2)
assert.Exactly(t, c.MustGet("intInterface").(int), 1)
}
func TestContextGetString(t *testing.T) {
@ -261,6 +250,18 @@ func TestContextGetInt64(t *testing.T) {
assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
}
func TestContextGetUint(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("uint", uint(1))
assert.Equal(t, uint(1), c.GetUint("uint"))
}
func TestContextGetUint64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("uint64", uint64(18446744073709551615))
assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64"))
}
func TestContextGetFloat64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2)
@ -288,7 +289,7 @@ func TestContextGetStringSlice(t *testing.T) {
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string]interface{})
m := make(map[string]any)
m["foo"] = 1
c.Set("map", m)
@ -298,7 +299,7 @@ func TestContextGetStringMap(t *testing.T) {
func TestContextGetStringMapString(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string]string)
m := make(map[string]string)
m["foo"] = "bar"
c.Set("map", m)
@ -308,7 +309,7 @@ func TestContextGetStringMapString(t *testing.T) {
func TestContextGetStringMapStringSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string][]string)
m := make(map[string][]string)
m["foo"] = []string{"foo"}
c.Set("map", m)
@ -357,15 +358,12 @@ func TestContextHandlerNames(t *testing.T) {
}
func handlerNameTest(c *Context) {
}
func handlerNameTest2(c *Context) {
}
var handlerTest HandlerFunc = func(c *Context) {
}
func TestContextHandler(t *testing.T) {
@ -410,6 +408,21 @@ func TestContextQuery(t *testing.T) {
assert.Empty(t, c.PostForm("foo"))
}
func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
assert.NotPanics(t, func() {
value, ok := c.GetQuery("NoKey")
assert.False(t, ok)
assert.Empty(t, value)
})
assert.NotPanics(t, func() {
assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada"))
})
assert.NotPanics(t, func() {
assert.Empty(t, c.Query("NoKey"))
})
}
func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
@ -632,26 +645,21 @@ func TestContextBodyAllowedForStatus(t *testing.T) {
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
}
type TestPanicRender struct {
type TestRender struct{}
func (*TestRender) Render(http.ResponseWriter) error {
return errTestRender
}
func (*TestPanicRender) Render(http.ResponseWriter) error {
return errors.New("TestPanicRender")
}
func (*TestRender) WriteContentType(http.ResponseWriter) {}
func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
func TestContextRenderPanicIfErr(t *testing.T) {
defer func() {
r := recover()
assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
}()
func TestContextRenderIfErr(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Render(http.StatusOK, &TestPanicRender{})
c.Render(http.StatusOK, &TestRender{})
assert.Fail(t, "Panic not detected")
assert.Equal(t, errorMsgs{&Error{Err: errTestRender, Type: 1}}, c.Errors)
}
// Tests that the response is serialized as JSON
@ -940,7 +948,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextData tests that the response can be written from `bytesting`
// TestContextData tests that the response can be written from `bytestring`
// with specified MIME type
func TestContextRenderData(t *testing.T) {
w := httptest.NewRecorder()
@ -991,7 +999,9 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
}
func TestContextRenderFileFromFS(t *testing.T) {
@ -1003,7 +1013,9 @@ func TestContextRenderFileFromFS(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.Equal(t, "/some/path", c.Request.URL.Path)
}
@ -1017,7 +1029,34 @@ func TestContextRenderAttachment(t *testing.T) {
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition"))
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition"))
}
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() *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)
newFilename := "new🧡_filename.go"
c.Request, _ = http.NewRequest("GET", "/", nil)
c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *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
@ -1033,6 +1072,19 @@ func TestContextRenderYAML(t *testing.T) {
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextRenderTOML tests that the response is serialized as TOML
// and Content-Type is set to application/toml
func TestContextRenderTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.TOML(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "foo = 'bar'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf
// and Content-Type is set to application/x-protobuf
// and we just use the example protobuf to check if the response is correct
@ -1153,6 +1205,36 @@ func TestContextNegotiationWithXML(t *testing.T) {
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEYAML, MIMEXML, MIMEJSON, MIMETOML},
Data: H{"foo": "bar"},
})
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"))
}
func TestContextNegotiationWithTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMETOML, MIMEXML, MIMEJSON, MIMEYAML},
Data: H{"foo": "bar"},
})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "foo = 'bar'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
@ -1241,6 +1323,14 @@ func TestContextNegotiationFormatCustom(t *testing.T) {
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON))
}
func TestContextNegotiationFormat2(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "image/tiff-fx")
assert.Equal(t, "", c.NegotiateFormat("image/tiff"))
}
func TestContextIsAborted(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.IsAborted())
@ -1255,7 +1345,7 @@ func TestContextIsAborted(t *testing.T) {
assert.True(t, c.IsAborted())
}
// TestContextData tests that the response can be written from `bytesting`
// TestContextData tests that the response can be written from `bytestring`
// with specified MIME type
func TestContextAbortWithStatus(t *testing.T) {
w := httptest.NewRecorder()
@ -1298,7 +1388,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
assert.Equal(t, "{\"foo\":\"fooValue\",\"bar\":\"barValue\"}", jsonStringBody)
}
func TestContextError(t *testing.T) {
@ -1306,12 +1396,12 @@ func TestContextError(t *testing.T) {
assert.Empty(t, c.Errors)
firstErr := errors.New("first error")
c.Error(firstErr) // nolint: errcheck
c.Error(firstErr) //nolint: errcheck
assert.Len(t, c.Errors, 1)
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
secondErr := errors.New("second error")
c.Error(&Error{ // nolint: errcheck
c.Error(&Error{ //nolint: errcheck
Err: secondErr,
Meta: "some data 2",
Type: ErrorTypePublic,
@ -1333,13 +1423,13 @@ func TestContextError(t *testing.T) {
t.Error("didn't panic")
}
}()
c.Error(nil) // nolint: errcheck
c.Error(nil) //nolint: errcheck
}
func TestContextTypedError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) //nolint: errcheck
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) //nolint: errcheck
for _, err := range c.Errors.ByType(ErrorTypePublic) {
assert.Equal(t, ErrorTypePublic, err.Type)
@ -1354,7 +1444,7 @@ func TestContextAbortWithError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") //nolint: errcheck
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, abortIndex, c.index)
@ -1364,12 +1454,11 @@ func TestContextAbortWithError(t *testing.T) {
func TestContextClientIP(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
// Legacy tests (validating that the defaults don't break the
// (insecure!) old behaviour)
assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
@ -1380,7 +1469,7 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP")
c.engine.AppEngine = true
c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
@ -1389,6 +1478,113 @@ func TestContextClientIP(t *testing.T) {
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
// Tests exercising the TrustedProxies functionality
resetContextForClientIPTests(c)
// IPv6 support
c.Request.RemoteAddr = "[::1]:12345"
assert.Equal(t, "20.20.20.20", c.ClientIP())
resetContextForClientIPTests(c)
// No trusted proxies
_ = c.engine.SetTrustedProxies([]string{})
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Disabled TrustedProxies feature
_ = c.engine.SetTrustedProxies(nil)
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Last proxy is trusted, but the RemoteAddr is not
_ = c.engine.SetTrustedProxies([]string{"30.30.30.30"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Only trust RemoteAddr
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
assert.Equal(t, "30.30.30.30", c.ClientIP())
// All steps are trusted
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30", "20.20.20.20"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use CIDR
_ = c.engine.SetTrustedProxies([]string{"40.40.25.25/16", "30.30.30.30"})
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Use hostname that resolves to all the proxies
_ = c.engine.SetTrustedProxies([]string{"foo"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Use hostname that returns an error
_ = c.engine.SetTrustedProxies([]string{"bar"})
assert.Equal(t, "40.40.40.40", c.ClientIP())
// X-Forwarded-For has a non-IP element
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Set("X-Forwarded-For", " blah ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Result from LookupHost has non-IP element. This should never
// happen, but we should test it to make sure we handle it
// gracefully.
_ = c.engine.SetTrustedProxies([]string{"baz"})
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
assert.Equal(t, "40.40.40.40", c.ClientIP())
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Del("X-Forwarded-For")
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.engine.RemoteIPHeaders = []string{}
c.engine.TrustedPlatform = PlatformGoogleAppEngine
assert.Equal(t, "50.50.50.50", c.ClientIP())
// Use custom TrustedPlatform header
c.engine.TrustedPlatform = "X-CDN-IP"
c.Request.Header.Set("X-CDN-IP", "80.80.80.80")
assert.Equal(t, "80.80.80.80", c.ClientIP())
// wrong header
c.engine.TrustedPlatform = "X-Wrong-Header"
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.Request.Header.Del("X-CDN-IP")
// TrustedPlatform is empty
c.engine.TrustedPlatform = ""
assert.Equal(t, "40.40.40.40", c.ClientIP())
// Test the legacy flag
c.engine.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.engine.AppEngine = false
c.engine.TrustedPlatform = PlatformGoogleAppEngine
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = PlatformCloudflare
assert.Equal(t, "60.60.60.60", c.ClientIP())
c.Request.Header.Del("CF-Connecting-IP")
assert.Equal(t, "40.40.40.40", c.ClientIP())
c.engine.TrustedPlatform = ""
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())
}
func resetContextForClientIPTests(c *Context) {
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.Header.Set("CF-Connecting-IP", "60.60.60.60")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
c.engine.TrustedPlatform = ""
c.engine.trustedCIDRs = defaultTrustedCIDRs
c.engine.AppEngine = false
}
func TestContextContentType(t *testing.T) {
@ -1430,6 +1626,7 @@ func TestContextBindWithJSON(t *testing.T) {
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1506,6 +1703,23 @@ func TestContextBindWithYAML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo = 'bar'\nbar = 'foo'"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `toml:"foo"`
Bar string `toml:"bar"`
}
assert.NoError(t, c.BindTOML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1639,6 +1853,23 @@ func TestContextShouldBindWithYAML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo='bar'\nbar= 'foo'"))
c.Request.Header.Add("Content-Type", MIMETOML) // set fake content-type
var obj struct {
Foo string `toml:"foo"`
Bar string `toml:"bar"`
}
assert.NoError(t, c.ShouldBindTOML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoShouldBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1746,6 +1977,7 @@ func TestContextGolangContext(t *testing.T) {
assert.Equal(t, ti, time.Time{})
assert.False(t, ok)
assert.Equal(t, c.Value(0), c.Request)
assert.Equal(t, c.Value(ContextKey), c)
assert.Nil(t, c.Value("foo"))
c.Set("foo", "bar")
@ -1916,8 +2148,8 @@ func TestRaceParamsContextCopy(t *testing.T) {
}(c.Copy(), c.Param("name"))
})
}
performRequest(router, "GET", "/name1/api")
performRequest(router, "GET", "/name2/api")
PerformRequest(router, "GET", "/name1/api")
PerformRequest(router, "GET", "/name2/api")
wg.Wait()
}
@ -1933,3 +2165,283 @@ func TestContextWithKeysMutex(t *testing.T) {
assert.Nil(t, value)
assert.False(t, err)
}
func TestRemoteIPFail(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.RemoteAddr = "[:::]:80"
ip := net.ParseIP(c.RemoteIP())
trust := c.engine.isTrustedProxy(ip)
assert.Nil(t, ip)
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
c.engine.ContextWithFallback = true
deadline, ok := c.Deadline()
assert.Zero(t, deadline)
assert.False(t, ok)
c2, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c2.engine.ContextWithFallback = true
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
d := time.Now().Add(time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
c2.Request = c2.Request.WithContext(ctx)
deadline, ok = c2.Deadline()
assert.Equal(t, d, deadline)
assert.True(t, ok)
}
func TestContextWithFallbackDoneFromRequestContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
assert.Nil(t, c.Done())
c2, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c2.engine.ContextWithFallback = true
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
c2.Request = c2.Request.WithContext(ctx)
cancel()
assert.NotNil(t, <-c2.Done())
}
func TestContextWithFallbackErrFromRequestContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
assert.Nil(t, c.Err())
c2, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c2.engine.ContextWithFallback = true
c2.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(context.Background())
c2.Request = c2.Request.WithContext(ctx)
cancel()
assert.EqualError(t, c2.Err(), context.Canceled.Error())
}
func TestContextWithFallbackValueFromRequestContext(t *testing.T) {
type contextKey string
tests := []struct {
name string
getContextAndKey func() (*Context, any)
value any
}{
{
name: "c with struct context key",
getContextAndKey: func() (*Context, any) {
var key struct{}
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), key, "value"))
return c, key
},
value: "value",
},
{
name: "c with string context key",
getContextAndKey: func() (*Context, any) {
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request = c.Request.WithContext(context.WithValue(context.TODO(), contextKey("key"), "value"))
return c, contextKey("key")
},
value: "value",
},
{
name: "c with nil http.Request",
getContextAndKey: func() (*Context, any) {
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
c.Request = nil
return c, "key"
},
value: nil,
},
{
name: "c with nil http.Request.Context()",
getContextAndKey: func() (*Context, any) {
c, _ := CreateTestContext(httptest.NewRecorder())
// enable ContextWithFallback feature flag
c.engine.ContextWithFallback = true
c.Request, _ = http.NewRequest("POST", "/", nil)
return c, "key"
},
value: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, key := tt.getContextAndKey()
assert.Equal(t, tt.value, c.Value(key))
})
}
}
func TestContextCopyShouldNotCancel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
ensureRequestIsOver := make(chan struct{})
wg := &sync.WaitGroup{}
r := New()
r.GET("/", func(ginctx *Context) {
wg.Add(1)
ginctx = ginctx.Copy()
// start async goroutine for calling srv
go func() {
defer wg.Done()
<-ensureRequestIsOver // ensure request is done
req, err := http.NewRequestWithContext(ginctx, http.MethodGet, srv.URL, nil)
must(err)
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(fmt.Errorf("request error: %w", err))
return
}
if res.StatusCode != http.StatusOK {
t.Error(fmt.Errorf("unexpected status code: %s", res.Status))
}
}()
})
l, err := net.Listen("tcp", ":0")
must(err)
go func() {
s := &http.Server{
Handler: r,
}
must(s.Serve(l))
}()
addr := strings.Split(l.Addr().String(), ":")
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
if err != nil {
t.Error(fmt.Errorf("request error: %w", err))
return
}
close(ensureRequestIsOver)
if res.StatusCode != http.StatusOK {
t.Error(fmt.Errorf("unexpected status code: %s", res.Status))
return
}
wg.Wait()
}
func TestContextAddParam(t *testing.T) {
c := &Context{}
id := "id"
value := "1"
c.AddParam(id, value)
v, ok := c.Params.Get(id)
assert.Equal(t, ok, true)
assert.Equal(t, value, v)
}
func TestCreateTestContextWithRouteParams(t *testing.T) {
w := httptest.NewRecorder()
engine := New()
engine.GET("/:action/:name", func(ctx *Context) {
ctx.String(http.StatusOK, "%s %s", ctx.Param("action"), ctx.Param("name"))
})
c := CreateTestContextOnly(w, engine)
c.Request, _ = http.NewRequest(http.MethodGet, "/hello/gin", nil)
engine.HandleContext(c)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "hello gin", w.Body.String())
}
type interceptedWriter struct {
ResponseWriter
b *bytes.Buffer
}
func (i interceptedWriter) WriteHeader(code int) {
i.Header().Del("X-Test")
i.ResponseWriter.WriteHeader(code)
}
func TestInterceptedHeader(t *testing.T) {
w := httptest.NewRecorder()
c, r := CreateTestContext(w)
r.Use(func(c *Context) {
i := interceptedWriter{
ResponseWriter: c.Writer,
b: bytes.NewBuffer(nil),
}
c.Writer = i
c.Next()
c.Header("X-Test", "overridden")
c.Writer = i.ResponseWriter
})
r.GET("/", func(c *Context) {
c.Header("X-Test", "original")
c.Header("X-Test-2", "present")
c.String(http.StatusOK, "hello world")
})
c.Request = httptest.NewRequest("GET", "/", nil)
r.HandleContext(c)
// Result() has headers frozen when WriteHeaderNow() has been called
// Compared to this time, this is when the response headers will be flushed
// As response is flushed on c.String, the Header cannot be set by the first
// middleware. Assert this
assert.Equal(t, "", w.Result().Header.Get("X-Test"))
assert.Equal(t, "present", w.Result().Header.Get("X-Test-2"))
}

View File

@ -12,7 +12,7 @@ import (
"strings"
)
const ginSupportMinGoVer = 10
const ginSupportMinGoVer = 18
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -50,7 +50,7 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
}
}
func debugPrint(format string, values ...interface{}) {
func debugPrint(format string, values ...any) {
if !IsDebugging() {
return
}
@ -63,6 +63,7 @@ func debugPrint(format string, values ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
@ -76,8 +77,8 @@ func getMinVer(v string) (uint64, error) {
}
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.18+.
`)
}
@ -105,9 +106,7 @@ at initialization. ie. before any route is registered or the router is listening
}
func debugPrintError(err error) {
if err != nil {
if IsDebugging() {
if err != nil && IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
}

View File

@ -5,13 +5,14 @@
package gin
import (
"bytes"
"errors"
"fmt"
"html/template"
"io"
"log"
"os"
"runtime"
"strings"
"sync"
"testing"
@ -20,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)
@ -64,6 +65,18 @@ func TestDebugPrintRoutes(t *testing.T) {
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
func TestDebugPrintRouteFunc(t *testing.T) {
DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] %-6s %-40s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
func TestDebugPrintLoadTemplate(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
@ -90,8 +103,8 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
SetMode(TestMode)
})
m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
if e == nil && m < ginSupportMinGoVer {
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)
}
@ -125,7 +138,7 @@ func captureOutput(t *testing.T, f func()) string {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
var buf bytes.Buffer
var buf strings.Builder
wg.Done()
_, err := io.Copy(&buf, reader)
assert.NoError(t, err)

View File

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

2243
docs/doc.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -34,12 +34,12 @@ const (
type Error struct {
Err error
Type ErrorType
Meta interface{}
Meta any
}
type errorMsgs []*Error
var _ error = &Error{}
var _ error = (*Error)(nil)
// SetType sets the error's type.
func (msg *Error) SetType(flags ErrorType) *Error {
@ -48,14 +48,14 @@ func (msg *Error) SetType(flags ErrorType) *Error {
}
// SetMeta sets the error's meta data.
func (msg *Error) SetMeta(data interface{}) *Error {
func (msg *Error) SetMeta(data any) *Error {
msg.Meta = data
return msg
}
// JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} {
json := H{}
func (msg *Error) JSON() any {
jsonData := H{}
if msg.Meta != nil {
value := reflect.ValueOf(msg.Meta)
switch value.Kind() {
@ -63,16 +63,16 @@ func (msg *Error) JSON() interface{} {
return msg.Meta
case reflect.Map:
for _, key := range value.MapKeys() {
json[key.String()] = value.MapIndex(key).Interface()
jsonData[key.String()] = value.MapIndex(key).Interface()
}
default:
json["meta"] = msg.Meta
jsonData["meta"] = msg.Meta
}
}
if _, ok := json["error"]; !ok {
json["error"] = msg.Error()
if _, ok := jsonData["error"]; !ok {
jsonData["error"] = msg.Error()
}
return json
return jsonData
}
// MarshalJSON implements the json.Marshaller interface.
@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0
}
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg *Error) Unwrap() error {
return msg.Err
}
// ByType returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
@ -117,8 +122,9 @@ func (a errorMsgs) Last() *Error {
return nil
}
// Errors returns an array will all the error messages.
// Errors returns an array with all the error messages.
// Example:
//
// c.Error(errors.New("first"))
// c.Error(errors.New("second"))
// c.Error(errors.New("third"))
@ -134,18 +140,18 @@ func (a errorMsgs) Errors() []string {
return errorStrings
}
func (a errorMsgs) JSON() interface{} {
switch len(a) {
func (a errorMsgs) JSON() any {
switch length := len(a); length {
case 0:
return nil
case 1:
return a.Last().JSON()
default:
json := make([]interface{}, len(a))
jsonData := make([]any, length)
for i, err := range a {
json[i] = err.JSON()
jsonData[i] = err.JSON()
}
return json
return jsonData
}
}

View File

@ -6,6 +6,7 @@ package gin
import (
"errors"
"fmt"
"testing"
"github.com/gin-gonic/gin/internal/json"
@ -34,7 +35,7 @@ func TestError(t *testing.T) {
jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ // nolint: errcheck
err.SetMeta(H{ //nolint: errcheck
"status": "200",
"data": "some data",
})
@ -44,7 +45,7 @@ func TestError(t *testing.T) {
"data": "some data",
}, err.JSON())
err.SetMeta(H{ // nolint: errcheck
err.SetMeta(H{ //nolint: errcheck
"error": "custom error",
"status": "200",
"data": "some data",
@ -59,7 +60,7 @@ func TestError(t *testing.T) {
status string
data string
}
err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
err.SetMeta(customError{status: "200", data: "other data"}) //nolint: errcheck
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
}
@ -85,7 +86,7 @@ Error #02: second
Error #03: third
Meta: map[status:400]
`, errs.String())
assert.Equal(t, []interface{}{
assert.Equal(t, []any{
H{"error": "first"},
H{"error": "second", "meta": "some data"},
H{"error": "third", "status": "400"},
@ -104,3 +105,24 @@ Error #03: third
assert.Nil(t, errs.JSON())
assert.Empty(t, errs.String())
}
type TestErr string
func (e TestErr) Error() string { return string(e) }
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("some error")
// 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
err := fmt.Errorf("wrapped: %w", &Error{
Err: innerErr,
Type: ErrorTypeAny,
})
// check that 'errors.Is()' and 'errors.As()' behave as expected :
assert.True(t, errors.Is(err, innerErr))
var testErr TestErr
assert.True(t, errors.As(err, &testErr))
}

10
fs.go
View File

@ -9,7 +9,7 @@ import (
"os"
)
type onlyfilesFS struct {
type onlyFilesFS struct {
fs http.FileSystem
}
@ -17,7 +17,7 @@ type neuteredReaddirFile struct {
http.File
}
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
// in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files.
@ -26,11 +26,11 @@ func Dir(root string, listDirectory bool) http.FileSystem {
if listDirectory {
return fs
}
return &onlyfilesFS{fs}
return &onlyFilesFS{fs}
}
// Open conforms to http.Filesystem.
func (fs onlyfilesFS) Open(name string) (http.File, error) {
func (fs onlyFilesFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
@ -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
}

294
gin.go
View File

@ -11,10 +11,14 @@ import (
"net/http"
"os"
"path"
"regexp"
"strings"
"sync"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
const defaultMultipartMemory = 32 << 20 // 32 MB
@ -22,16 +26,31 @@ const defaultMultipartMemory = 32 << 20 // 32 MB
var (
default404Body = []byte("404 page not found")
default405Body = []byte("405 method not allowed")
defaultAppEngine bool
)
var defaultPlatform string
var defaultTrustedCIDRs = []*net.IPNet{
{ // 0.0.0.0/0 (IPv4)
IP: net.IP{0x0, 0x0, 0x0, 0x0},
Mask: net.IPMask{0x0, 0x0, 0x0, 0x0},
},
{ // ::/0 (IPv6)
IP: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
Mask: net.IPMask{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
},
}
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)
// HandlersChain defines a HandlerFunc array.
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main one.
// Last returns the last handler in the chain. i.e. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
@ -47,22 +66,32 @@ type RouteInfo struct {
HandlerFunc HandlerFunc
}
// RoutesInfo defines a RouteInfo array.
// RoutesInfo defines a RouteInfo slice.
type RoutesInfo []RouteInfo
// Trusted platforms
const (
// PlatformGoogleAppEngine when running on Google App Engine. Trust X-Appengine-Remote-Addr
// for determining the client's IP
PlatformGoogleAppEngine = "X-Appengine-Remote-Addr"
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP
PlatformCloudflare = "CF-Connecting-IP"
)
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
RouterGroup
// Enables automatic redirection if the current route can't be matched but a
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
// If enabled, the router tries to fix the current request path, if no
// RedirectFixedPath if enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
@ -73,37 +102,60 @@ type Engine struct {
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// If enabled, the router checks if another method is allowed for the
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
// ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that
// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
// fetched, it falls back to the IP obtained from
// `(*gin.Context).Request.RemoteAddr`.
ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with
// AppEngine was deprecated.
// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD
// #726 #755 If enabled, it will trust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool
// If enabled, the url.RawPath will be used to find parameters.
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
UseRawPath bool
// If true, the path value will be unescaped.
// UnescapePathValues if true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call.
MaxMultipartMemory int64
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
// See the PR #1817 and issue #1644
RemoveExtraSlash bool
// RemoteIPHeaders list of headers used to obtain the client IP when
// `(*gin.Engine).ForwardedByClientIP` is `true` and
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
RemoteIPHeaders []string
// TrustedPlatform if set to a constant of value gin.Platform*, trusts the headers set by
// that platform, for example to determine the client IP
TrustedPlatform string
// MaxMultipartMemory value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call.
MaxMultipartMemory int64
// UseH2C enable h2c support.
UseH2C bool
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
ContextWithFallback bool
delims render.Delims
secureJsonPrefix string
secureJSONPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
@ -112,12 +164,16 @@ type Engine struct {
noMethod HandlersChain
pool sync.Pool
trees methodTrees
maxParams uint16
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
}
var _ IRouter = &Engine{}
var _ IRouter = (*Engine)(nil)
// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// By default, the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
@ -137,18 +193,21 @@ func New() *Engine {
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
@ -161,19 +220,30 @@ func Default() *Engine {
return engine
}
func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine, KeysMutex: &sync.RWMutex{}}
func (engine *Engine) Handler() http.Handler {
if !engine.UseH2C {
return engine
}
h2s := &http2.Server{}
return h2c.NewHandler(engine, h2s)
}
// Delims sets template left and right delims and returns a Engine instance.
func (engine *Engine) allocateContext(maxParams uint16) *Context {
v := make(Params, 0, maxParams)
skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
// Delims sets template left and right delims and returns an Engine instance.
func (engine *Engine) Delims(left, right string) *Engine {
engine.delims = render.Delims{Left: left, Right: right}
return engine
}
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON.
// SecureJsonPrefix sets the secureJSONPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix
engine.secureJSONPrefix = prefix
return engine
}
@ -219,19 +289,19 @@ func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.FuncMap = funcMap
}
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
// NoRoute adds handlers for NoRoute. It returns a 404 code by default.
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.noRoute = handlers
engine.rebuild404Handlers()
}
// NoMethod sets the handlers called when... TODO.
// NoMethod sets the handlers called when Engine.HandleMethodNotAllowed = true.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.noMethod = handlers
engine.rebuild405Handlers()
}
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
@ -255,6 +325,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
root := engine.trees.get(method)
if root == nil {
root = new(node)
@ -262,6 +333,15 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
// Routes returns a slice of registered routes, including some useful information, such as:
@ -296,12 +376,120 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
err = http.ListenAndServe(address, engine.Handler())
return
}
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.trustedProxies == nil {
return nil, nil
}
cidr := make([]*net.IPNet, 0, len(engine.trustedProxies))
for _, trustedProxy := range engine.trustedProxies {
if !strings.Contains(trustedProxy, "/") {
ip := parseIP(trustedProxy)
if ip == nil {
return cidr, &net.ParseError{Type: "IP address", Text: trustedProxy}
}
switch len(ip) {
case net.IPv4len:
trustedProxy += "/32"
case net.IPv6len:
trustedProxy += "/128"
}
}
_, cidrNet, err := net.ParseCIDR(trustedProxy)
if err != nil {
return cidr, err
}
cidr = append(cidr, cidrNet)
}
return cidr, nil
}
// SetTrustedProxies set a list of network origins (IPv4 addresses,
// IPv4 CIDRs, IPv6 addresses or IPv6 CIDRs) from which to trust
// request's headers that contain alternative client IP when
// `(*gin.Engine).ForwardedByClientIP` is `true`. `TrustedProxies`
// feature is enabled by default, and it also trusts all proxies
// by default. If you want to disable this feature, use
// Engine.SetTrustedProxies(nil), then Context.ClientIP() will
// return the remote address directly.
func (engine *Engine) SetTrustedProxies(trustedProxies []string) error {
engine.trustedProxies = trustedProxies
return engine.parseTrustedProxies()
}
// isUnsafeTrustedProxies checks if Engine.trustedCIDRs contains all IPs, it's not safe if it has (returns true)
func (engine *Engine) isUnsafeTrustedProxies() bool {
return engine.isTrustedProxy(net.ParseIP("0.0.0.0")) || engine.isTrustedProxy(net.ParseIP("::"))
}
// parseTrustedProxies parse Engine.trustedProxies to Engine.trustedCIDRs
func (engine *Engine) parseTrustedProxies() error {
trustedCIDRs, err := engine.prepareTrustedCIDRs()
engine.trustedCIDRs = trustedCIDRs
return err
}
// isTrustedProxy will check whether the IP address is included in the trusted list according to Engine.trustedCIDRs
func (engine *Engine) isTrustedProxy(ip net.IP) bool {
if engine.trustedCIDRs == nil {
return false
}
for _, cidr := range engine.trustedCIDRs {
if cidr.Contains(ip) {
return true
}
}
return false
}
// validateHeader will parse X-Forwarded-For header and return the trusted client IP address
func (engine *Engine) validateHeader(header string) (clientIP string, valid bool) {
if header == "" {
return "", false
}
items := strings.Split(header, ",")
for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr)
if ip == nil {
break
}
// X-Forwarded-For is appended by proxy
// Check IPs in reverse order and stop when find untrusted proxy
if (i == 0) || (!engine.isTrustedProxy(ip)) {
return ipStr, true
}
}
return "", false
}
// parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP {
parsedIP := net.ParseIP(ip)
if ipv4 := parsedIP.To4(); ipv4 != nil {
// return ip in a 4-byte representation
return ipv4
}
// return ip in a 16-byte representation or nil
return parsedIP
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
@ -309,17 +497,27 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving HTTPS on %s\n", addr)
defer func() { debugPrintError(err) }()
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
return
}
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file).
// through the specified unix socket (i.e. a file).
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
listener, err := net.Listen("unix", file)
if err != nil {
return
@ -327,7 +525,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
defer listener.Close()
defer os.Remove(file)
err = http.Serve(listener, engine)
err = http.Serve(listener, engine.Handler())
return
}
@ -338,6 +536,11 @@ func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://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))
listener, err := net.FileListener(f)
if err != nil {
@ -353,7 +556,13 @@ func (engine *Engine) RunFd(fd int) (err error) {
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
err = http.Serve(listener, engine)
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.Serve(listener, engine.Handler())
return
}
@ -369,9 +578,9 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c)
}
// HandleContext re-enter a context that has been rewritten.
// HandleContext re-enters a context that has been rewritten.
// This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
// Disclaimer: You can loop yourself to deal with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
c.reset()
@ -401,16 +610,18 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.Params, unescape)
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if httpMethod != http.MethodConnect && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
@ -427,7 +638,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
@ -461,6 +672,9 @@ func redirectTrailingSlash(c *Context) {
req := c.Request
p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
prefix = regSafePrefix.ReplaceAllString(prefix, "")
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
p = prefix + "/" + req.URL.Path
}
req.URL.Path = p + "/"

View File

@ -37,7 +37,7 @@ func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ)
}
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
// NoRoute adds handlers for NoRoute. It returns a 404 code by default.
func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...)
}
@ -108,6 +108,7 @@ func StaticFile(relativePath, filepath string) gin.IRoutes {
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use :
//
// router.Static("/static", "/var/www")
func Static(relativePath, root string) gin.IRoutes {
return engine().Static(relativePath, root)
@ -118,7 +119,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs)
}
// Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
// Use attaches a global middleware to the router. i.e. the middlewares attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
@ -145,7 +146,7 @@ func RunTLS(addr, certFile, keyFile string) (err error) {
}
// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file)
// through the specified unix socket (i.e. a file)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunUnix(file string) (err error) {
return engine().RunUnix(file)

View File

@ -9,11 +9,13 @@ import (
"crypto/tls"
"fmt"
"html/template"
"io/ioutil"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
"time"
@ -21,7 +23,15 @@ import (
"github.com/stretchr/testify/assert"
)
func testRequest(t *testing.T, url string) {
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
// params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content) default:"it worked"
func testRequest(t *testing.T, params ...string) {
if len(params) == 0 {
t.Fatal("url cannot be empty")
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
@ -29,14 +39,27 @@ func testRequest(t *testing.T, url string) {
}
client := &http.Client{Transport: tr}
resp, err := client.Get(url)
resp, err := client.Get(params[0])
assert.NoError(t, err)
defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body)
body, ioerr := io.ReadAll(resp.Body)
assert.NoError(t, ioerr)
assert.Equal(t, "it worked", string(body), "resp body should match")
assert.Equal(t, "200 OK", resp.Status, "should get a 200")
var responseStatus = "200 OK"
if len(params) > 1 && params[1] != "" {
responseStatus = params[1]
}
var responseBody = "it worked"
if len(params) > 2 && params[2] != "" {
responseBody = params[2]
}
assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
if responseStatus == "200 OK" {
assert.Equal(t, responseBody, string(body), "resp body should match")
}
}
func TestRunEmpty(t *testing.T) {
@ -54,6 +77,81 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example")
}
func TestBadTrustedCIDRs(t *testing.T) {
router := New()
assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
}
/* legacy tests
func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080"))
}
func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunUnix(unixTestSocket))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router := New()
router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
}
func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
os.Setenv("PORT", "")
router := New()
router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}
*/
func TestRunTLS(t *testing.T) {
router := New()
go func() {
@ -146,15 +244,19 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) {
router := New()
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunUnix("/tmp/unix_unit_test"))
assert.NoError(t, router.RunUnix(unixTestSocket))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", "/tmp/unix_unit_test")
c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
@ -180,7 +282,16 @@ func TestFileDescriptor(t *testing.T) {
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
if isWindows() {
// not supported by windows, it is unimplemented now
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if socketFile == nil {
return
}
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -300,3 +411,153 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}
func TestTreeRunDynamicRouting(t *testing.T) {
router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") })
router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") })
router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") })
router.GET("/c1/:dd/e", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e") })
router.GET("/c1/:dd/e1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e1") })
router.GET("/c1/:dd/f1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f1") })
router.GET("/c1/:dd/f2", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/f2") })
router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") })
router.GET("/:cc/:dd/ee", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/ee") })
router.GET("/:cc/:dd/f", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/f") })
router.GET("/:cc/:dd/:ee/ff", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/ff") })
router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") })
router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") })
router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") })
router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") })
router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") })
router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") })
router.GET("/get/abc", func(c *Context) { c.String(http.StatusOK, "/get/abc") })
router.GET("/get/:param", func(c *Context) { c.String(http.StatusOK, "/get/:param") })
router.GET("/get/abc/123abc", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc") })
router.GET("/get/abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param") })
router.GET("/get/abc/123abc/xxx8", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8") })
router.GET("/get/abc/123abc/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/:param") })
router.GET("/get/abc/123abc/xxx8/1234", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234") })
router.GET("/get/abc/123abc/xxx8/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/:param") })
router.GET("/get/abc/123abc/xxx8/1234/ffas", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/ffas") })
router.GET("/get/abc/123abc/xxx8/1234/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/:param") })
router.GET("/get/abc/123abc/xxx8/1234/kkdd/12c", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/12c") })
router.GET("/get/abc/123abc/xxx8/1234/kkdd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abc/xxx8/1234/kkdd/:param") })
router.GET("/get/abc/:param/test", func(c *Context) { c.String(http.StatusOK, "/get/abc/:param/test") })
router.GET("/get/abc/123abd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abd/:param") })
router.GET("/get/abc/123abddd/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abddd/:param") })
router.GET("/get/abc/123/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123/:param") })
router.GET("/get/abc/123abg/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abg/:param") })
router.GET("/get/abc/123abf/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abf/:param") })
router.GET("/get/abc/123abfff/:param", func(c *Context) { c.String(http.StatusOK, "/get/abc/123abfff/:param") })
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/", "", "home")
testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx")
testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx")
testRequest(t, ts.URL+"/all", "", "/:cc")
testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/c1/d/e", "", "/c1/:dd/e")
testRequest(t, ts.URL+"/c1/d/e1", "", "/c1/:dd/e1")
testRequest(t, ts.URL+"/c1/d/ee", "", "/:cc/:dd/ee")
testRequest(t, ts.URL+"/c1/d/f", "", "/:cc/:dd/f")
testRequest(t, ts.URL+"/c/d/ee", "", "/:cc/:dd/ee")
testRequest(t, ts.URL+"/c/d/e/ff", "", "/:cc/:dd/:ee/ff")
testRequest(t, ts.URL+"/c/d/e/f/gg", "", "/:cc/:dd/:ee/:ff/gg")
testRequest(t, ts.URL+"/c/d/e/f/g/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
testRequest(t, ts.URL+"/a", "", "/:cc")
testRequest(t, ts.URL+"/d", "", "/:cc")
testRequest(t, ts.URL+"/ad", "", "/:cc")
testRequest(t, ts.URL+"/dd", "", "/:cc")
testRequest(t, ts.URL+"/aa", "", "/:cc")
testRequest(t, ts.URL+"/aaa", "", "/:cc")
testRequest(t, ts.URL+"/aaa/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/ab", "", "/:cc")
testRequest(t, ts.URL+"/abb", "", "/:cc")
testRequest(t, ts.URL+"/abb/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/dddaa", "", "/:cc")
testRequest(t, ts.URL+"/allxxxx", "", "/:cc")
testRequest(t, ts.URL+"/alldd", "", "/:cc")
testRequest(t, ts.URL+"/cc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/ccc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/deedwjfs/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/acllcc/cc", "", "/:cc/cc")
testRequest(t, ts.URL+"/get/test/abc/", "", "/get/test/abc/")
testRequest(t, ts.URL+"/get/testaa/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/te/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/xx/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/tt/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/a/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/")
testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test")
testRequest(t, ts.URL+"/something/secondthingaaaa/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/se/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/s/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/something/secondthing/thirdthing", "", "/something/:paramname/thirdthing")
testRequest(t, ts.URL+"/get/abc", "", "/get/abc")
testRequest(t, ts.URL+"/get/a", "", "/get/:param")
testRequest(t, ts.URL+"/get/abz", "", "/get/:param")
testRequest(t, ts.URL+"/get/12a", "", "/get/:param")
testRequest(t, ts.URL+"/get/abcd", "", "/get/:param")
testRequest(t, ts.URL+"/get/abc/123abc", "", "/get/abc/123abc")
testRequest(t, ts.URL+"/get/abc/12", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123ab", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/xyz", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123abcddxx", "", "/get/abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8", "", "/get/abc/123abc/xxx8")
testRequest(t, ts.URL+"/get/abc/123abc/x", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/abc", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8xxas", "", "/get/abc/123abc/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234", "", "/get/abc/123abc/xxx8/1234")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/123", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/78k", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234xxxd", "", "/get/abc/123abc/xxx8/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas", "", "/get/abc/123abc/xxx8/1234/ffas")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/f", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffa", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kka", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/ffas321", "", "/get/abc/123abc/xxx8/1234/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c", "", "/get/abc/123abc/xxx8/1234/kkdd/12c")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/1", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12b", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/34", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", "", "/get/abc/123abc/xxx8/1234/kkdd/:param")
testRequest(t, ts.URL+"/get/abc/12/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abdd/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abdddf/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123ab/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abgg/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abff/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abffff/test", "", "/get/abc/:param/test")
testRequest(t, ts.URL+"/get/abc/123abd/test", "", "/get/abc/123abd/:param")
testRequest(t, ts.URL+"/get/abc/123abddd/test", "", "/get/abc/123abddd/:param")
testRequest(t, ts.URL+"/get/abc/123/test22", "", "/get/abc/123/:param")
testRequest(t, ts.URL+"/get/abc/123abg/test", "", "/get/abc/123abg/:param")
testRequest(t, ts.URL+"/get/abc/123abf/testss", "", "/get/abc/123abf/:param")
testRequest(t, ts.URL+"/get/abc/123abfff/te", "", "/get/abc/123abfff/:param")
// 404 not found
testRequest(t, ts.URL+"/c/d/e", "404 Not Found")
testRequest(t, ts.URL+"/c/d/e1", "404 Not Found")
testRequest(t, ts.URL+"/c/d/eee", "404 Not Found")
testRequest(t, ts.URL+"/c1/d/eee", "404 Not Found")
testRequest(t, ts.URL+"/c1/d/e2", "404 Not Found")
testRequest(t, ts.URL+"/cc/dd/ee/ff/gg/hh1", "404 Not Found")
testRequest(t, ts.URL+"/a/dd", "404 Not Found")
testRequest(t, ts.URL+"/addr/dd/aa", "404 Not Found")
testRequest(t, ts.URL+"/something/secondthing/121", "404 Not Found")
}
func isWindows() bool {
return runtime.GOOS == "windows"
}

View File

@ -8,7 +8,8 @@ import (
"crypto/tls"
"fmt"
"html/template"
"io/ioutil"
"io"
"net"
"net/http"
"net/http/httptest"
"reflect"
@ -18,6 +19,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"golang.org/x/net/http2"
)
func formatAsDate(t time.Time) string {
@ -41,7 +43,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
})
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
@ -71,10 +73,48 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestH2c(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Error(err)
}
r := Default()
r.UseH2C = true
r.GET("/", func(c *Context) {
c.String(200, "<h1>Hello world</h1>")
})
go func() {
err := http.Serve(ln, r.Handler())
if err != nil {
t.Log(err)
}
}()
defer ln.Close()
url := "http://" + ln.Addr().String() + "/"
httpClient := http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(netw, addr)
},
},
}
res, err := httpClient.Get(url)
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -91,10 +131,10 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -111,10 +151,10 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -138,10 +178,10 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -158,11 +198,11 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp))
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
func init() {
@ -189,10 +229,10 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -209,10 +249,10 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -229,10 +269,10 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -256,10 +296,10 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
@ -276,11 +316,11 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil {
fmt.Println(err)
t.Error(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp))
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
func TestAddRoute(t *testing.T) {
@ -394,7 +434,6 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
}
func TestRebuild404Handlers(t *testing.T) {
}
func TestNoMethodWithGlobalHandlers(t *testing.T) {
@ -428,7 +467,7 @@ func TestNoMethodWithGlobalHandlers(t *testing.T) {
compareFunc(t, router.allNoMethod[2], middleware0)
}
func compareFunc(t *testing.T, a, b interface{}) {
func compareFunc(t *testing.T, a, b any) {
sf1 := reflect.ValueOf(a)
sf2 := reflect.ValueOf(b)
if sf1.Pointer() != sf2.Pointer() {
@ -490,7 +529,7 @@ func TestEngineHandleContext(t *testing.T) {
}
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/")
w := PerformRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code)
})
}
@ -523,7 +562,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
})
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
w := PerformRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len())
})
@ -532,6 +571,119 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New()
// valid ipv4 cidr
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv4 cidr
{
err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
assert.Error(t, err)
}
// valid ipv4 address
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
err := r.SetTrustedProxies([]string{"192.168.1.33"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv4 address
{
err := r.SetTrustedProxies([]string{"192.168.1.256"})
assert.Error(t, err)
}
// valid ipv6 address
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv6 address
{
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.Error(t, err)
}
// valid ipv6 cidr
{
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid ipv6 cidr
{
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
assert.Error(t, err)
}
// valid combination
{
expectedTrustedCIDRs := []*net.IPNet{
parseCIDR("::/0"),
parseCIDR("192.168.0.0/16"),
parseCIDR("172.16.0.1/32"),
}
err := r.SetTrustedProxies([]string{
"::/0",
"192.168.0.0/16",
"172.16.0.1",
})
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
}
// invalid combination
{
err := r.SetTrustedProxies([]string{
"::/0",
"192.168.0.0/16",
"172.16.0.256",
})
assert.Error(t, err)
}
// nil value
{
err := r.SetTrustedProxies(nil)
assert.Nil(t, r.trustedCIDRs)
assert.Nil(t, err)
}
}
func parseCIDR(cidr string) *net.IPNet {
_, parsedCIDR, err := net.ParseCIDR(cidr)
if err != nil {
fmt.Println(err)
}
return parsedCIDR
}
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {

View File

@ -5,12 +5,12 @@
package gin
import (
"bytes"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -291,18 +291,18 @@ func TestShouldBindUri(t *testing.T) {
type Person struct {
Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"`
ID string `uri:"id" binding:"required"`
}
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id)
assert.True(t, person.Name != "")
assert.True(t, person.ID != "")
c.String(http.StatusOK, "ShouldBindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, http.MethodGet, path)
w := PerformRequest(router, http.MethodGet, path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
@ -313,18 +313,18 @@ func TestBindUri(t *testing.T) {
type Person struct {
Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"`
ID string `uri:"id" binding:"required"`
}
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.BindUri(&person))
assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id)
assert.True(t, person.Name != "")
assert.True(t, person.ID != "")
c.String(http.StatusOK, "BindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, http.MethodGet, path)
w := PerformRequest(router, http.MethodGet, path)
assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
@ -342,7 +342,7 @@ func TestBindUriError(t *testing.T) {
})
path1, _ := exampleFromPath("/new/rest/:num")
w1 := performRequest(router, http.MethodGet, path1)
w1 := PerformRequest(router, http.MethodGet, path1)
assert.Equal(t, http.StatusBadRequest, w1.Code)
}
@ -358,7 +358,7 @@ func TestRaceContextCopy(t *testing.T) {
go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics")
})
w := performRequest(router, http.MethodGet, "/test/copy/race")
w := PerformRequest(router, http.MethodGet, "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String())
}
@ -389,7 +389,7 @@ func TestGithubAPI(t *testing.T) {
for _, route := range githubAPI {
path, values := exampleFromPath(route.path)
w := performRequest(router, route.method, path)
w := PerformRequest(router, route.method, path)
// TEST
assert.Contains(t, w.Body.String(), "\"status\":\"good\"")
@ -401,7 +401,7 @@ func TestGithubAPI(t *testing.T) {
}
func exampleFromPath(path string) (string, Params) {
output := new(bytes.Buffer)
output := new(strings.Builder)
params := make(Params, 0, 6)
start := -1
for i, c := range path {

38
go.mod
View File

@ -1,14 +1,36 @@
module github.com/gin-gonic/gin
go 1.13
go 1.20
require (
github.com/bytedance/sonic v1.9.1
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.3
github.com/json-iterator/go v1.1.9
github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7
gopkg.in/yaml.v2 v2.2.8
github.com/go-playground/validator/v10 v10.14.0
github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.19
github.com/pelletier/go-toml/v2 v2.0.8
github.com/stretchr/testify v1.8.3
github.com/ugorji/go/codec v1.2.11
golang.org/x/net v0.10.0
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // 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.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
)

107
go.sum
View File

@ -1,45 +1,90 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.0 h1:iwLYBds8bYtzwOX7kmdYwtS+aY2GgekVoIs2/IxR0tM=
github.com/bytedance/sonic v1.9.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
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/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.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/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.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
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/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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/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.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/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=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,19 +0,0 @@
package bytesconv
import (
"reflect"
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) (b []byte) {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
return b
}
// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

@ -0,0 +1,26 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.20
package bytesconv
import (
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}
// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

@ -0,0 +1,23 @@
// Copyright 2023 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go1.20
package bytesconv
import (
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// BytesToString converts byte slice to string without a memory allocation.
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}

View File

@ -1,3 +1,7 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package bytesconv
import (

22
internal/json/go_json.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go_json
package json
import json "github.com/goccy/go-json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !jsoniter
//go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
package json

View File

@ -2,11 +2,11 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build jsoniter
//go:build jsoniter
package json
import "github.com/json-iterator/go"
import jsoniter "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary

23
internal/json/sonic.go Normal file
View File

@ -0,0 +1,23 @@
// 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 sonic && avx && (linux || windows || darwin) && amd64
package json
import "github.com/bytedance/sonic"
var (
json = sonic.ConfigStd
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -44,7 +44,7 @@ type LoggerConfig struct {
// Optional. Default value is gin.DefaultWriter.
Output io.Writer
// SkipPaths is a url path array which logs are not written.
// SkipPaths is an url path array which logs are not written.
// Optional.
SkipPaths []string
}
@ -70,12 +70,12 @@ type LogFormatterParams struct {
Path string
// ErrorMessage is set if error has occurred in processing the request.
ErrorMessage string
// isTerm shows whether does gin's output descriptor refers to a terminal.
// isTerm shows whether gin's output descriptor refers to a terminal.
isTerm bool
// BodySize is the size of the Response Body
BodySize int
// Keys are the keys set on the request's context.
Keys map[string]interface{}
Keys map[string]any
}
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
@ -138,8 +138,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
}
if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
param.Latency = param.Latency.Truncate(time.Second)
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
@ -162,12 +161,12 @@ func ForceConsoleColor() {
consoleColorMode = forceColor
}
// ErrorLogger returns a handlerfunc for any error type.
// ErrorLogger returns a HandlerFunc for any error type.
func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAny)
}
// ErrorLoggerT returns a handlerfunc for a given error type.
// ErrorLoggerT returns a HandlerFunc for a given error type.
func ErrorLoggerT(typ ErrorType) HandlerFunc {
return func(c *Context) {
c.Next()
@ -179,7 +178,7 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
}
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
// By default, gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}

View File

@ -5,10 +5,10 @@
package gin
import (
"bytes"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"time"
@ -20,7 +20,7 @@ func init() {
}
func TestLogger(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithWriter(buffer))
router.GET("/example", func(c *Context) {})
@ -31,7 +31,7 @@ func TestLogger(t *testing.T) {
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
PerformRequest(router, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
@ -41,50 +41,50 @@ func TestLogger(t *testing.T) {
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
PerformRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example")
PerformRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example")
PerformRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example")
PerformRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example")
PerformRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
PerformRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound")
PerformRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithConfig(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
router.GET("/example", func(c *Context) {})
@ -95,7 +95,7 @@ func TestLoggerWithConfig(t *testing.T) {
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
PerformRequest(router, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
@ -105,50 +105,50 @@ func TestLoggerWithConfig(t *testing.T) {
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
PerformRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example")
PerformRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example")
PerformRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example")
PerformRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example")
PerformRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
PerformRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound")
PerformRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithFormatter(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
d := DefaultWriter
DefaultWriter = buffer
@ -169,7 +169,7 @@ func TestLoggerWithFormatter(t *testing.T) {
)
}))
router.GET("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
PerformRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
@ -181,10 +181,12 @@ func TestLoggerWithFormatter(t *testing.T) {
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
var gotKeys map[string]interface{}
buffer := new(bytes.Buffer)
var gotKeys map[string]any
buffer := new(strings.Builder)
router := New()
router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Formatter: func(param LogFormatterParams) string {
@ -206,8 +208,9 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
// set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
gotKeys = c.Keys
time.Sleep(time.Millisecond)
})
performRequest(router, "GET", "/example?a=100")
PerformRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
@ -226,7 +229,6 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys)
}
func TestDefaultLogFormatter(t *testing.T) {
@ -280,7 +282,6 @@ func TestDefaultLogFormatter(t *testing.T) {
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
}
func TestColorForMethod(t *testing.T) {
@ -357,46 +358,46 @@ func TestErrorLogger(t *testing.T) {
router := New()
router.Use(ErrorLogger())
router.GET("/error", func(c *Context) {
c.Error(errors.New("this is an error")) // nolint: errcheck
c.Error(errors.New("this is an error")) //nolint: errcheck
})
router.GET("/abort", func(c *Context) {
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) //nolint: errcheck
})
router.GET("/print", func(c *Context) {
c.Error(errors.New("this is an error")) // nolint: errcheck
c.Error(errors.New("this is an error")) //nolint: errcheck
c.String(http.StatusInternalServerError, "hola!")
})
w := performRequest(router, "GET", "/error")
w := PerformRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort")
w = PerformRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print")
w = PerformRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithWriter(buffer, "/skipped"))
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged")
PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped")
PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
@ -405,11 +406,11 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged")
PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped")
PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}

View File

@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
signature += " XX "
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusOK, w.Code)
@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
signature += " X "
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
signature += " XX "
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -118,7 +118,10 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
func TestMiddlewareNoMethodDisabled(t *testing.T) {
signature := ""
router := New()
// NoMethod disabled
router.HandleMethodNotAllowed = false
router.Use(func(c *Context) {
signature += "A"
c.Next()
@ -144,8 +147,9 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
@ -171,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusUnauthorized, w.Code)
@ -186,14 +190,13 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
c.Next()
c.AbortWithStatus(http.StatusGone)
signature += "B"
})
router.GET("/", func(c *Context) {
signature += "C"
c.Next()
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusGone, w.Code)
@ -208,7 +211,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New()
router.Use(func(context *Context) {
signature += "A"
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) //nolint: errcheck
})
router.Use(func(context *Context) {
signature += "B"
@ -216,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C"
})
// RUN
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
@ -243,7 +246,7 @@ func TestMiddlewareWrite(t *testing.T) {
})
})
w := performRequest(router, "GET", "/")
w := PerformRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))

17
mode.go
View File

@ -5,6 +5,7 @@
package gin
import (
"flag"
"io"
"os"
@ -22,6 +23,7 @@ const (
// TestMode indicates gin mode is test.
TestMode = "test"
)
const (
debugCode = iota
releaseCode
@ -33,6 +35,7 @@ const (
// Note that both Logger and Recovery provides custom ways to configure their
// output io.Writer.
// To support coloring in Windows use:
//
// import "github.com/mattn/go-colorable"
// gin.DefaultWriter = colorable.NewColorableStdout()
var DefaultWriter io.Writer = os.Stdout
@ -40,8 +43,10 @@ var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr
var ginMode = debugCode
var modeName = DebugMode
var (
ginMode = debugCode
modeName = DebugMode
)
func init() {
mode := os.Getenv(EnvGinMode)
@ -51,8 +56,12 @@ func init() {
// SetMode sets gin mode according to input string.
func SetMode(value string) {
if value == "" {
if flag.Lookup("test.v") != nil {
value = TestMode
} else {
value = DebugMode
}
}
switch value {
case DebugMode:
@ -62,7 +71,7 @@ func SetMode(value string) {
case TestMode:
ginMode = testCode
default:
panic("gin mode unknown: " + value)
panic("gin mode unknown: " + value + " (available mode: debug release test)")
}
modeName = value
@ -85,7 +94,7 @@ func EnableJsonDecoderDisallowUnknownFields() {
binding.EnableDecoderDisallowUnknownFields = true
}
// Mode returns currently gin mode.
// Mode returns current gin mode.
func Mode() string {
return modeName
}

View File

@ -5,6 +5,7 @@
package gin
import (
"flag"
"os"
"testing"
@ -21,9 +22,16 @@ func TestSetMode(t *testing.T) {
assert.Equal(t, TestMode, Mode())
os.Unsetenv(EnvGinMode)
SetMode("")
assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode())
tmp := flag.CommandLine
flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
SetMode("")
assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode())
flag.CommandLine = tmp
SetMode(DebugMode)
assert.Equal(t, debugCode, ginMode)

View File

@ -136,10 +136,11 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
// Otherwise use either the stack buffer, if it is large enough, or
// allocate a new buffer on the heap, and copy all previous characters.
if l := len(s); l > cap(b) {
*buf = make([]byte, len(s))
length := len(s)
if length > cap(b) {
*buf = make([]byte, length)
} else {
*buf = (*buf)[:l]
*buf = (*buf)[:length]
}
b = *buf

View File

@ -6,9 +6,9 @@ package gin
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@ -26,13 +26,29 @@ var (
slash = []byte("/")
)
// RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any)
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
// CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter, handle)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc {
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
if len(recovery) > 0 {
return CustomRecoveryWithWriter(out, recovery[0])
}
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
@ -44,8 +60,11 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
var se *os.SyscallError
if errors.As(ne, &se) {
seStr := strings.ToLower(se.Error())
if strings.Contains(seStr, "broken pipe") ||
strings.Contains(seStr, "connection reset by peer") {
brokenPipe = true
}
}
@ -60,23 +79,23 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
timeFormat(time.Now()), headersToStr, err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
// If the connection is dead, we can't write a status to it.
if brokenPipe {
c.Error(err.(error)) // nolint: errcheck
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) //nolint: errcheck
c.Abort()
} else {
c.AbortWithStatus(http.StatusInternalServerError)
handle(c, err)
}
}
}()
@ -84,6 +103,10 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
}
}
func defaultHandleRecovery(c *Context, _ any) {
c.AbortWithStatus(http.StatusInternalServerError)
}
// stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
@ -99,7 +122,7 @@ func stack(skip int) []byte {
// Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile {
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
continue
}
@ -133,7 +156,7 @@ func function(pc uintptr) []byte {
// runtime/debug.*T·ptrmethod
// and want
// *T.ptrmethod
// Also the package path might contains dot (e.g. code.google.com/...),
// Also the package path might contain dot (e.g. code.google.com/...),
// so first eliminate the path prefix
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
name = name[lastSlash+1:]
@ -141,11 +164,11 @@ 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
}
// timeFormat returns a customized time string for logger.
func timeFormat(t time.Time) string {
var timeString = t.Format("2006/01/02 - 15:04:05")
return timeString
return t.Format("2006/01/02 - 15:04:05")
}

View File

@ -5,7 +5,6 @@
package gin
import (
"bytes"
"fmt"
"net"
"net/http"
@ -18,7 +17,7 @@ import (
)
func TestPanicClean(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
router := New()
password := "my-super-secret-password"
router.Use(RecoveryWithWriter(buffer))
@ -27,7 +26,7 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery",
w := PerformRequest(router, "GET", "/recovery",
header{
Key: "Host",
Value: "www.google.com",
@ -50,25 +49,25 @@ func TestPanicClean(t *testing.T) {
// TestPanicInHandler assert that panic has been recovered.
func TestPanicInHandler(t *testing.T) {
buffer := new(bytes.Buffer)
buffer := new(strings.Builder)
router := New()
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
w := PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = performRequest(router, "GET", "/recovery")
w = PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@ -85,21 +84,21 @@ func TestPanicWithAbort(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
w := PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, []byte("???"), bs)
assert.Equal(t, dunno, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, []byte("???"), bs)
assert.Equal(t, dunno, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
@ -107,7 +106,7 @@ func TestSource(t *testing.T) {
func TestFunction(t *testing.T) {
bs := function(1)
assert.Equal(t, []byte("???"), bs)
assert.Equal(t, dunno, bs)
}
// TestPanicWithBrokenPipe asserts that recovery specifically handles
@ -122,8 +121,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) {
var buf bytes.Buffer
var buf strings.Builder
router := New()
router.Use(RecoveryWithWriter(&buf))
@ -137,10 +135,114 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e)
})
// RUN
w := performRequest(router, "GET", "/recovery")
w := PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
})
}
}
func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)
router := New()
handleRecovery := func(c *Context, err any) {
errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest)
}
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
func TestCustomRecovery(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)
router := New()
DefaultErrorWriter = buffer
handleRecovery := func(c *Context, err any) {
errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest)
}
router.Use(CustomRecovery(handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)
router := New()
DefaultErrorWriter = buffer
handleRecovery := func(c *Context, err any) {
errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest)
}
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}

View File

@ -20,7 +20,7 @@ type Delims struct {
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
type HTMLRender interface {
// Instance returns an HTML instance.
Instance(string, interface{}) Render
Instance(string, any) Render
}
// HTMLProduction contains template reference and its delims.
@ -41,13 +41,13 @@ type HTMLDebug struct {
type HTML struct {
Template *template.Template
Name string
Data interface{}
Data any
}
var htmlContentType = []string{"text/html; charset=utf-8"}
// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface.
func (r HTMLProduction) Instance(name string, data interface{}) Render {
func (r HTMLProduction) Instance(name string, data any) Render {
return HTML{
Template: r.Template,
Name: name,
@ -56,7 +56,7 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
}
// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface.
func (r HTMLDebug) Instance(name string, data interface{}) Render {
func (r HTMLDebug) Instance(name string, data any) Render {
return HTML{
Template: r.loadTemplate(),
Name: name,

View File

@ -16,49 +16,45 @@ import (
// JSON contains the given interface object.
type JSON struct {
Data interface{}
Data any
}
// IndentedJSON contains the given interface object.
type IndentedJSON struct {
Data interface{}
Data any
}
// SecureJSON contains the given interface object and its prefix.
type SecureJSON struct {
Prefix string
Data interface{}
Data any
}
// JsonpJSON contains the given interface object its callback.
type JsonpJSON struct {
Callback string
Data interface{}
Data any
}
// AsciiJSON contains the given interface object.
type AsciiJSON struct {
Data interface{}
Data any
}
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string
// PureJSON contains the given interface object.
type PureJSON struct {
Data interface{}
Data any
}
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
var (
jsonContentType = []string{"application/json; charset=utf-8"}
jsonpContentType = []string{"application/javascript; charset=utf-8"}
jsonASCIIContentType = []string{"application/json"}
)
// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil {
panic(err)
}
return
func (r JSON) Render(w http.ResponseWriter) error {
return WriteJSON(w, r.Data)
}
// WriteContentType (JSON) writes JSON ContentType.
@ -67,7 +63,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
}
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
func WriteJSON(w http.ResponseWriter, obj any) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
if err != nil {
@ -103,8 +99,7 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
// if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
bytesconv.StringToBytes("]")) {
_, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes(r.Prefix)); err != nil {
return err
}
}
@ -131,20 +126,19 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
}
callback := template.JSEscapeString(r.Callback)
_, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil {
return err
}
_, err = w.Write(bytesconv.StringToBytes("("))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
return err
}
_, err = w.Write(ret)
if err != nil {
if _, err = w.Write(ret); err != nil {
return err
}
_, err = w.Write(bytesconv.StringToBytes(");"))
if err != nil {
if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
return err
}
@ -179,7 +173,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
// WriteContentType (AsciiJSON) writes JSON ContentType.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
writeContentType(w, jsonASCIIContentType)
}
// Render (PureJSON) writes custom ContentType and encodes the given interface object.

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
//go:build !nomsgpack
package render
@ -12,13 +12,15 @@ import (
"github.com/ugorji/go/codec"
)
// Check interface implemented here to support go build tag nomsgpack.
// See: https://github.com/gin-gonic/gin/pull/1852/
var (
_ Render = MsgPack{}
)
// MsgPack contains the given interface object.
type MsgPack struct {
Data interface{}
Data any
}
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
@ -34,7 +36,7 @@ func (r MsgPack) Render(w http.ResponseWriter) error {
}
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
func WriteMsgPack(w http.ResponseWriter, obj any) error {
writeContentType(w, msgpackContentType)
var mh codec.MsgpackHandle
return codec.NewEncoder(w, &mh).Encode(obj)

View File

@ -7,12 +7,12 @@ package render
import (
"net/http"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
)
// ProtoBuf contains the given interface object.
type ProtoBuf struct {
Data interface{}
Data any
}
var protobufContentType = []string{"application/x-protobuf"}

View File

@ -30,6 +30,7 @@ var (
_ Render = Reader{}
_ Render = AsciiJSON{}
_ Render = ProtoBuf{}
_ Render = TOML{}
)
func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !nomsgpack
//go:build !nomsgpack
package render
@ -20,7 +20,7 @@ import (
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
data := map[string]any{
"foo": "bar",
}
@ -38,6 +38,6 @@ func TestRenderMsgPack(t *testing.T) {
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -8,16 +8,17 @@ import (
"encoding/xml"
"errors"
"html/template"
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"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"
)
// TODO unit tests
@ -25,7 +26,7 @@ import (
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
data := map[string]any{
"foo": "bar",
"html": "<b>",
}
@ -40,17 +41,17 @@ func TestRenderJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderJSONPanics(t *testing.T) {
func TestRenderJSONError(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
// json: unsupported type: chan int
assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
assert.Error(t, (JSON{data}).Render(w))
}
func TestRenderIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
data := map[string]any{
"foo": "bar",
"bar": "foo",
}
@ -73,7 +74,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
func TestRenderSecureJSON(t *testing.T) {
w1 := httptest.NewRecorder()
data := map[string]interface{}{
data := map[string]any{
"foo": "bar",
}
@ -87,7 +88,7 @@ func TestRenderSecureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
datas := []map[string]interface{}{{
datas := []map[string]any{{
"foo": "bar",
}, {
"bar": "foo",
@ -110,7 +111,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
func TestRenderJsonpJSON(t *testing.T) {
w1 := httptest.NewRecorder()
data := map[string]interface{}{
data := map[string]any{
"foo": "bar",
}
@ -124,7 +125,7 @@ func TestRenderJsonpJSON(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
datas := []map[string]interface{}{{
datas := []map[string]any{{
"foo": "bar",
}, {
"bar": "foo",
@ -136,9 +137,54 @@ 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]interface{}{
data := map[string]any{
"foo": "bar",
}
(JsonpJSON{"", data}).WriteContentType(w)
@ -162,7 +208,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
func TestRenderAsciiJSON(t *testing.T) {
w1 := httptest.NewRecorder()
data1 := map[string]interface{}{
data1 := map[string]any{
"lang": "GO语言",
"tag": "<br>",
}
@ -174,7 +220,7 @@ func TestRenderAsciiJSON(t *testing.T) {
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
data2 := float64(3.1415926)
data2 := 3.1415926
err = (AsciiJSON{data2}).Render(w2)
assert.NoError(t, err)
@ -191,7 +237,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
func TestRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
data := map[string]any{
"foo": "bar",
"html": "<b>",
}
@ -201,7 +247,7 @@ func TestRenderPureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
type xmlmap map[string]interface{}
type xmlmap map[string]any
// Allows type H to be used with xml.Marshal
func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
@ -238,14 +284,14 @@ b:
err := (YAML{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "\"\\na : Easy!\\nb:\\n\\tc: 2\\n\\td: [3, 4]\\n\\t\"\n", w.Body.String())
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
type fail struct{}
// Hook MarshalYAML
func (ft *fail) MarshalYAML() (interface{}, error) {
func (ft *fail) MarshalYAML() (any, error) {
return nil, errors.New("fail")
}
@ -255,6 +301,27 @@ func TestRenderYAMLFail(t *testing.T) {
assert.Error(t, err)
}
func TestRenderTOML(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
"html": "<b>",
}
(TOML{data}).WriteContentType(w)
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
err := (TOML{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderTOMLFail(t *testing.T) {
w := httptest.NewRecorder()
err := (TOML{net.IPv4bcast}).Render(w)
assert.Error(t, err)
}
// test Protobuf rendering
func TestRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder()
@ -359,13 +426,13 @@ func TestRenderString(t *testing.T) {
(String{
Format: "hello %s %d",
Data: []interface{}{},
Data: []any{},
}).WriteContentType(w)
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
err := (String{
Format: "hola %s %d",
Data: []interface{}{"manu", 2},
Data: []any{"manu", 2},
}).Render(w)
assert.NoError(t, err)
@ -378,7 +445,7 @@ func TestRenderStringLenZero(t *testing.T) {
err := (String{
Format: "hola %s %d",
Data: []interface{}{},
Data: []any{},
}).Render(w)
assert.NoError(t, err)
@ -391,7 +458,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("t", map[string]interface{}{
instance := htmlRender.Instance("t", map[string]any{
"name": "alexandernyquist",
})
@ -407,7 +474,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
templ := template.Must(template.New("").Parse(`Hello {{.name}}`))
htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("", map[string]interface{}{
instance := htmlRender.Instance("", map[string]any{
"name": "alexandernyquist",
})
@ -420,12 +487,13 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
@ -438,12 +506,13 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: nil,
htmlRender := HTMLDebug{
Files: nil,
Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
@ -455,7 +524,8 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
}
func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{Files: nil,
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
Delims: Delims{"{{", "}}"},
FuncMap: nil,

View File

@ -6,14 +6,15 @@ package render
import (
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
)
// String contains the given interface object slice and its format.
type String struct {
Format string
Data []interface{}
Data []any
}
var plainContentType = []string{"text/plain; charset=utf-8"}
@ -29,12 +30,12 @@ func (r String) WriteContentType(w http.ResponseWriter) {
}
// WriteString writes data according to its format and write custom ContentType.
func WriteString(w http.ResponseWriter, format string, data []interface{}) (err error) {
func WriteString(w http.ResponseWriter, format string, data []any) (err error) {
writeContentType(w, plainContentType)
if len(data) > 0 {
_, err = fmt.Fprintf(w, format, data...)
return
}
_, err = io.WriteString(w, format)
_, err = w.Write(bytesconv.StringToBytes(format))
return
}

36
render/toml.go Normal file
View File

@ -0,0 +1,36 @@
// 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.
package render
import (
"net/http"
"github.com/pelletier/go-toml/v2"
)
// TOML contains the given interface object.
type TOML struct {
Data any
}
var TOMLContentType = []string{"application/toml; charset=utf-8"}
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
func (r TOML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := toml.Marshal(r.Data)
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType (TOML) writes TOML ContentType for response.
func (r TOML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, TOMLContentType)
}

View File

@ -11,7 +11,7 @@ import (
// XML contains the given interface object.
type XML struct {
Data interface{}
Data any
}
var xmlContentType = []string{"application/xml; charset=utf-8"}

View File

@ -7,12 +7,12 @@ package render
import (
"net/http"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// YAML contains the given interface object.
type YAML struct {
Data interface{}
Data any
}
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}

View File

@ -23,23 +23,23 @@ type ResponseWriter interface {
http.Flusher
http.CloseNotifier
// Returns the HTTP response status code of the current request.
// Status returns the HTTP response status code of the current request.
Status() int
// Returns the number of bytes already written into the response http body.
// Size returns the number of bytes already written into the response http body.
// See Written()
Size() int
// Writes the string into the response body.
// WriteString writes the string into the response body.
WriteString(string) (int, error)
// Returns true if the response body was already written.
// Written returns true if the response body was already written.
Written() bool
// Forces to write the http header (status code + headers).
// WriteHeaderNow forces to write the http header (status code + headers).
WriteHeaderNow()
// get the http.Pusher for server push
// Pusher get the http.Pusher for server push
Pusher() http.Pusher
}
@ -49,7 +49,11 @@ type responseWriter struct {
status int
}
var _ ResponseWriter = &responseWriter{}
var _ ResponseWriter = (*responseWriter)(nil)
func (w *responseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
func (w *responseWriter) reset(writer http.ResponseWriter) {
w.ResponseWriter = writer
@ -61,6 +65,7 @@ func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code {
if w.Written() {
debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
return
}
w.status = code
}
@ -107,12 +112,12 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack()
}
// CloseNotify implements the http.CloseNotify interface.
// CloseNotify implements the http.CloseNotifier interface.
func (w *responseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Flush implements the http.Flush interface.
// Flush implements the http.Flusher interface.
func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()

View File

@ -17,17 +17,25 @@ import (
// func (w *responseWriter) CloseNotify() <-chan bool {
// func (w *responseWriter) Flush() {
var _ ResponseWriter = &responseWriter{}
var _ http.ResponseWriter = &responseWriter{}
var _ http.ResponseWriter = ResponseWriter(&responseWriter{})
var _ http.Hijacker = ResponseWriter(&responseWriter{})
var _ http.Flusher = ResponseWriter(&responseWriter{})
var _ http.CloseNotifier = ResponseWriter(&responseWriter{})
var (
_ ResponseWriter = &responseWriter{}
_ http.ResponseWriter = &responseWriter{}
_ http.ResponseWriter = ResponseWriter(&responseWriter{})
_ http.Hijacker = ResponseWriter(&responseWriter{})
_ http.Flusher = ResponseWriter(&responseWriter{})
_ http.CloseNotifier = ResponseWriter(&responseWriter{})
)
func init() {
SetMode(TestMode)
}
func TestResponseWriterUnwrap(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{ResponseWriter: testWriter}
assert.Same(t, testWriter, writer.Unwrap())
}
func TestResponseWriterReset(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
@ -130,3 +138,21 @@ func TestResponseWriterFlush(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
}
func TestResponseWriterStatusCode(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
writer.reset(testWriter)
w := ResponseWriter(writer)
w.WriteHeader(http.StatusOK)
w.WriteHeaderNow()
assert.Equal(t, http.StatusOK, w.Status())
assert.True(t, w.Written())
w.WriteHeader(http.StatusUnauthorized)
// status must be 200 although we tried to change it
assert.Equal(t, http.StatusOK, w.Status())
}

View File

@ -11,6 +11,18 @@ import (
"strings"
)
var (
// regEnLetter matches english letters for http method name
regEnLetter = regexp.MustCompile("^[A-Z]+$")
// anyMethods for RouterGroup Any method
anyMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
http.MethodTrace,
}
)
// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
IRoutes
@ -30,8 +42,10 @@ type IRoutes interface {
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
Match([]string, string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
StaticFileFS(string, string, http.FileSystem) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
@ -45,7 +59,7 @@ type RouterGroup struct {
root bool
}
var _ IRouter = &RouterGroup{}
var _ IRouter = (*RouterGroup)(nil)
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
@ -87,43 +101,43 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
if matched := regEnLetter.MatchString(httpMethod); !matched {
panic("http method " + httpMethod + " is not valid")
}
return group.handle(httpMethod, relativePath, handlers)
}
// POST is a shortcut for router.Handle("POST", path, handle).
// POST is a shortcut for router.Handle("POST", path, handlers).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle).
// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
// DELETE is a shortcut for router.Handle("DELETE", path, handlers).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodDelete, relativePath, handlers)
}
// PATCH is a shortcut for router.Handle("PATCH", path, handle).
// PATCH is a shortcut for router.Handle("PATCH", path, handlers).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPatch, relativePath, handlers)
}
// PUT is a shortcut for router.Handle("PUT", path, handle).
// PUT is a shortcut for router.Handle("PUT", path, handlers).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPut, relativePath, handlers)
}
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handlers).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodOptions, relativePath, handlers)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle).
// HEAD is a shortcut for router.Handle("HEAD", path, handlers).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodHead, relativePath, handlers)
}
@ -131,27 +145,43 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRo
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
group.handle(http.MethodGet, relativePath, handlers)
group.handle(http.MethodPost, relativePath, handlers)
group.handle(http.MethodPut, relativePath, handlers)
group.handle(http.MethodPatch, relativePath, handlers)
group.handle(http.MethodHead, relativePath, handlers)
group.handle(http.MethodOptions, relativePath, handlers)
group.handle(http.MethodDelete, relativePath, handlers)
group.handle(http.MethodConnect, relativePath, handlers)
group.handle(http.MethodTrace, relativePath, handlers)
for _, method := range anyMethods {
group.handle(method, relativePath, handlers)
}
return group.returnObj()
}
// Match registers a route that matches the specified methods that you declared.
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
for _, method := range methods {
group.handle(method, relativePath, handlers)
}
return group.returnObj()
}
// StaticFile registers a single route in order to serve a single file of the local filesystem.
// router.StaticFile("favicon.ico", "./resources/favicon.ico")
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
return group.staticFileHandler(relativePath, func(c *Context) {
c.File(filepath)
})
}
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
// Gin by default uses: gin.Dir()
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
return group.staticFileHandler(relativePath, func(c *Context) {
c.FileFromFS(filepath, fs)
})
}
func (group *RouterGroup) staticFileHandler(relativePath string, handler HandlerFunc) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static file")
}
handler := func(c *Context) {
c.File(filepath)
}
group.GET(relativePath, handler)
group.HEAD(relativePath, handler)
return group.returnObj()
@ -162,13 +192,14 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use :
//
// router.Static("/static", "/var/www")
func (group *RouterGroup) Static(relativePath, root string) IRoutes {
return group.StaticFS(relativePath, Dir(root, false))
}
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
// Gin by default user: gin.Dir()
// Gin by default uses: gin.Dir()
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static folder")
@ -187,7 +218,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) {
if _, nolisting := fs.(*onlyfilesFS); nolisting {
if _, noListing := fs.(*onlyFilesFS); noListing {
c.Writer.WriteHeader(http.StatusNotFound)
}
@ -209,9 +240,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)

View File

@ -80,11 +80,11 @@ func performRequestInGroup(t *testing.T, method string) {
panic("unknown method")
}
w := performRequest(router, method, "/v1/login/test")
w := PerformRequest(router, method, "/v1/login/test")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "the method was "+method+" and index 3", w.Body.String())
w = performRequest(router, method, "/v1/test")
w = PerformRequest(router, method, "/v1/test")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
}
@ -111,16 +111,31 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
})
}
func TestRouterGroupTooManyHandlers(t *testing.T) {
func TestRouterGroupInvalidStaticFileFS(t *testing.T) {
router := New()
handlers1 := make([]HandlerFunc, 40)
assert.Panics(t, func() {
router.StaticFileFS("/path/:param", "favicon.ico", Dir(".", false))
})
assert.Panics(t, func() {
router.StaticFileFS("/path/*param", "favicon.ico", Dir(".", false))
})
}
func TestRouterGroupTooManyHandlers(t *testing.T) {
const (
panicValue = "too many handlers"
maximumCnt = abortIndex
)
router := New()
handlers1 := make([]HandlerFunc, maximumCnt-1)
router.Use(handlers1...)
handlers2 := make([]HandlerFunc, 26)
assert.Panics(t, func() {
handlers2 := make([]HandlerFunc, maximumCnt+1)
assert.PanicsWithValue(t, panicValue, func() {
router.Use(handlers2...)
})
assert.Panics(t, func() {
assert.PanicsWithValue(t, panicValue, func() {
router.GET("/", handlers2...)
})
}
@ -171,8 +186,10 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
assert.Equal(t, r, r.PUT("/", handler))
assert.Equal(t, r, r.OPTIONS("/", handler))
assert.Equal(t, r, r.HEAD("/", handler))
assert.Equal(t, r, r.Match([]string{http.MethodPut, http.MethodPatch}, "/match", handler))
assert.Equal(t, r, r.StaticFile("/file", "."))
assert.Equal(t, r, r.StaticFileFS("/static2", ".", Dir(".", false)))
assert.Equal(t, r, r.Static("/static", "."))
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
}

View File

@ -6,7 +6,6 @@ package gin
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
@ -21,7 +20,8 @@ type header struct {
Value string
}
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
// PerformRequest for testing gin router.
func PerformRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, nil)
for _, h := range headers {
req.Header.Add(h.Key, h.Value)
@ -42,11 +42,11 @@ func testRouteOK(method string, t *testing.T) {
passed = true
})
w := performRequest(r, method, "/test")
w := PerformRequest(r, method, "/test")
assert.True(t, passed)
assert.Equal(t, http.StatusOK, w.Code)
performRequest(r, method, "/test2")
PerformRequest(r, method, "/test2")
assert.True(t, passedAny)
}
@ -58,7 +58,7 @@ func testRouteNotOK(method string, t *testing.T) {
passed = true
})
w := performRequest(router, method, "/test")
w := PerformRequest(router, method, "/test")
assert.False(t, passed)
assert.Equal(t, http.StatusNotFound, w.Code)
@ -79,7 +79,7 @@ func testRouteNotOK2(method string, t *testing.T) {
passed = true
})
w := performRequest(router, method, "/test")
w := PerformRequest(router, method, "/test")
assert.False(t, passed)
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -99,7 +99,7 @@ func TestRouterMethod(t *testing.T) {
c.String(http.StatusOK, "sup3")
})
w := performRequest(router, http.MethodPut, "/hey")
w := PerformRequest(router, http.MethodPut, "/hey")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "called", w.Body.String())
@ -150,50 +150,98 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
router.POST("/path3", func(c *Context) {})
router.PUT("/path4/", func(c *Context) {})
w := performRequest(router, http.MethodGet, "/path/")
w := PerformRequest(router, http.MethodGet, "/path/")
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, http.MethodGet, "/path2")
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, http.MethodPost, "/path3/")
w = PerformRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, "/path3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, http.MethodPut, "/path4")
w = PerformRequest(router, http.MethodPut, "/path4")
assert.Equal(t, "/path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, http.MethodGet, "/path")
w = PerformRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, http.MethodGet, "/path2/")
w = PerformRequest(router, http.MethodGet, "/path2/")
assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, http.MethodPost, "/path3")
w = PerformRequest(router, http.MethodPost, "/path3")
assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, http.MethodPut, "/path4/")
w = PerformRequest(router, http.MethodPut, "/path4/")
assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = performRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, 200, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"})
assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
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)
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)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"})
assert.Equal(t, "//path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"})
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"})
assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"})
assert.Equal(t, "api/api/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"})
assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"})
assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
router.RedirectTrailingSlash = false
w = performRequest(router, http.MethodGet, "/path/")
w = PerformRequest(router, http.MethodGet, "/path/")
assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, http.MethodGet, "/path2")
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, http.MethodPost, "/path3/")
w = PerformRequest(router, http.MethodPost, "/path3/")
assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, http.MethodPut, "/path4")
w = PerformRequest(router, http.MethodPut, "/path4")
assert.Equal(t, http.StatusNotFound, w.Code)
}
@ -207,19 +255,19 @@ func TestRouteRedirectFixedPath(t *testing.T) {
router.POST("/PATH3", func(c *Context) {})
router.POST("/Path4/", func(c *Context) {})
w := performRequest(router, http.MethodGet, "/PATH")
w := PerformRequest(router, http.MethodGet, "/PATH")
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, http.MethodGet, "/path2")
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, "/Path2", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, http.MethodPost, "/path3")
w = PerformRequest(router, http.MethodPost, "/path3")
assert.Equal(t, "/PATH3", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, http.MethodPost, "/path4")
w = PerformRequest(router, http.MethodPost, "/path4")
assert.Equal(t, "/Path4/", w.Header().Get("Location"))
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
}
@ -238,7 +286,6 @@ func TestRouteParamsByName(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@ -249,7 +296,7 @@ func TestRouteParamsByName(t *testing.T) {
assert.False(t, ok)
})
w := performRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
w := PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
@ -272,7 +319,6 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
@ -283,7 +329,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.False(t, ok)
})
w := performRequest(router, http.MethodGet, "//test//john//smith//is//super//great")
w := PerformRequest(router, http.MethodGet, "//test//john//smith//is//super//great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
@ -295,7 +341,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
func TestRouteStaticFile(t *testing.T) {
// SETUP file
testRoot, _ := os.Getwd()
f, err := ioutil.TempFile(testRoot, "")
f, err := os.CreateTemp(testRoot, "")
if err != nil {
t.Error(err)
}
@ -311,16 +357,50 @@ func TestRouteStaticFile(t *testing.T) {
router.Static("/using_static", dir)
router.StaticFile("/result", f.Name())
w := performRequest(router, http.MethodGet, "/using_static/"+filename)
w2 := performRequest(router, http.MethodGet, "/result")
w := PerformRequest(router, http.MethodGet, "/using_static/"+filename)
w2 := PerformRequest(router, http.MethodGet, "/result")
assert.Equal(t, w, w2)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
w3 := performRequest(router, http.MethodHead, "/using_static/"+filename)
w4 := performRequest(router, http.MethodHead, "/result")
w3 := PerformRequest(router, http.MethodHead, "/using_static/"+filename)
w4 := PerformRequest(router, http.MethodHead, "/result")
assert.Equal(t, w3, w4)
assert.Equal(t, http.StatusOK, w3.Code)
}
// TestHandleStaticFile - ensure the static file handles properly
func TestRouteStaticFileFS(t *testing.T) {
// SETUP file
testRoot, _ := os.Getwd()
f, err := os.CreateTemp(testRoot, "")
if err != nil {
t.Error(err)
}
defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err)
f.Close()
dir, filename := filepath.Split(f.Name())
// SETUP gin
router := New()
router.Static("/using_static", dir)
router.StaticFileFS("/result_fs", filename, Dir(dir, false))
w := PerformRequest(router, http.MethodGet, "/using_static/"+filename)
w2 := PerformRequest(router, http.MethodGet, "/result_fs")
assert.Equal(t, w, w2)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
w3 := PerformRequest(router, http.MethodHead, "/using_static/"+filename)
w4 := PerformRequest(router, http.MethodHead, "/result_fs")
assert.Equal(t, w3, w4)
assert.Equal(t, http.StatusOK, w3.Code)
@ -331,7 +411,7 @@ func TestRouteStaticListingDir(t *testing.T) {
router := New()
router.StaticFS("/", Dir("./", true))
w := performRequest(router, http.MethodGet, "/")
w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go")
@ -343,7 +423,7 @@ func TestRouteStaticNoListing(t *testing.T) {
router := New()
router.Static("/", "./")
w := performRequest(router, http.MethodGet, "/")
w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go")
@ -358,11 +438,13 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
})
static.Static("/", "./")
w := performRequest(router, http.MethodGet, "/gin.go")
w := PerformRequest(router, http.MethodGet, "/gin.go")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
@ -372,13 +454,13 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
router.POST("/path", func(c *Context) {})
w := performRequest(router, http.MethodGet, "/path")
w := PerformRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText")
})
w = performRequest(router, http.MethodGet, "/path")
w = PerformRequest(router, http.MethodGet, "/path")
assert.Equal(t, "responseText", w.Body.String())
assert.Equal(t, http.StatusTeapot, w.Code)
}
@ -389,7 +471,7 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
// add one methodTree to trees
router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
router.GET("/path2", func(c *Context) {})
w := performRequest(router, http.MethodPost, "/path2")
w := PerformRequest(router, http.MethodPost, "/path2")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
}
@ -397,13 +479,13 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {})
w := performRequest(router, http.MethodGet, "/path")
w := PerformRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusNotFound, w.Code)
router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText")
})
w = performRequest(router, http.MethodGet, "/path")
w = PerformRequest(router, http.MethodGet, "/path")
assert.Equal(t, "404 page not found", w.Body.String())
assert.Equal(t, http.StatusNotFound, w.Code)
}
@ -423,7 +505,7 @@ func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route)
w := PerformRequest(router, "GET", tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
@ -453,7 +535,7 @@ func TestRouterNotFound(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := performRequest(router, http.MethodGet, tr.route)
w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
@ -466,21 +548,36 @@ func TestRouterNotFound(t *testing.T) {
c.AbortWithStatus(http.StatusNotFound)
notFound = true
})
w := performRequest(router, http.MethodGet, "/nope")
w := PerformRequest(router, http.MethodGet, "/nope")
assert.Equal(t, http.StatusNotFound, w.Code)
assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {})
w = performRequest(router, http.MethodPatch, "/path/")
w = PerformRequest(router, http.MethodPatch, "/path/")
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists
router = New()
router.GET("/a", func(c *Context) {})
w = performRequest(router, http.MethodGet, "/")
w = PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusNotFound, w.Code)
// Reproduction test for the bug of issue #2843
router = New()
router.NoRoute(func(c *Context) {
if c.Request.RequestURI == "/login" {
c.String(200, "login")
}
})
router.GET("/logout", func(c *Context) {
c.String(200, "logout")
})
w = PerformRequest(router, http.MethodGet, "/login")
assert.Equal(t, "login", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/logout")
assert.Equal(t, "logout", w.Body.String())
}
func TestRouterStaticFSNotFound(t *testing.T) {
@ -490,10 +587,10 @@ func TestRouterStaticFSNotFound(t *testing.T) {
c.String(404, "non existent")
})
w := performRequest(router, http.MethodGet, "/nonexistent")
w := PerformRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
w = performRequest(router, http.MethodHead, "/nonexistent")
w = PerformRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
}
@ -503,7 +600,7 @@ func TestRouterStaticFSFileNotFound(t *testing.T) {
router.StaticFS("/", http.FileSystem(http.Dir(".")))
assert.NotPanics(t, func() {
performRequest(router, http.MethodGet, "/nonexistent")
PerformRequest(router, http.MethodGet, "/nonexistent")
})
}
@ -514,17 +611,17 @@ func TestMiddlewareCalledOnceByRouterStaticFSNotFound(t *testing.T) {
// Middleware must be called just only once by per request.
middlewareCalledNum := 0
router.Use(func(c *Context) {
middlewareCalledNum += 1
middlewareCalledNum++
})
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
// First access
performRequest(router, http.MethodGet, "/nonexistent")
PerformRequest(router, http.MethodGet, "/nonexistent")
assert.Equal(t, 1, middlewareCalledNum)
// Second access
performRequest(router, http.MethodHead, "/nonexistent")
PerformRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, 2, middlewareCalledNum)
}
@ -543,7 +640,7 @@ func TestRouteRawPath(t *testing.T) {
assert.Equal(t, "222", num)
})
w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/222")
w := PerformRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/222")
assert.Equal(t, http.StatusOK, w.Code)
}
@ -563,7 +660,7 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
assert.Equal(t, "333", num)
})
w := performRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/333")
w := PerformRequest(route, http.MethodPost, "/project/Some%2FOther%2FProject/build/333")
assert.Equal(t, http.StatusOK, w.Code)
}
@ -574,7 +671,7 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
c.Next()
})
w := performRequest(route, http.MethodGet, "/NotFound")
w := PerformRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len())
}
@ -593,6 +690,9 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
"/simple-two/one-two",
"/project/:name/build/*params",
"/project/:name/bui",
"/user/:id/status",
"/user/:id",
"/user/:id/profile",
}
for _, route := range routes {
@ -605,7 +705,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
}
for _, route := range routes {
w := performRequest(router, http.MethodGet, route)
w := PerformRequest(router, http.MethodGet, route)
assert.Equal(t, http.StatusOK, w.Code)
}
@ -615,6 +715,25 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
assert.Equal(t, "", c.FullPath())
})
w := performRequest(router, http.MethodGet, "/not-found")
w := PerformRequest(router, http.MethodGet, "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) {
r := New()
r.HandleMethodNotAllowed = true
base := r.Group("base")
base.GET("/metrics", handlerTest1)
v1 := base.Group("v1")
v1.GET("/:id/devices", handlerTest1)
v1.GET("/user/:id/groups", handlerTest1)
v1.GET("/orgs/:id", handlerTest1)
v1.DELETE("/orgs/:id", handlerTest1)
w := PerformRequest(r, "GET", "/base/v1/user/groups")
assert.Equal(t, http.StatusNotFound, w.Code)
}

View File

@ -9,7 +9,15 @@ import "net/http"
// CreateTestContext returns a fresh engine and context for testing purposes
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
r = New()
c = r.allocateContext()
c = r.allocateContext(0)
c.reset()
c.writermem.reset(w)
return
}
// CreateTestContextOnly returns a fresh context base on the engine for testing purposes
func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
c = r.allocateContext(r.maxParams)
c.reset()
c.writermem.reset(w)
return

View File

@ -1,24 +1,24 @@
// Code generated by protoc-gen-go.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.0
// protoc v3.15.8
// source: test.proto
// DO NOT EDIT!
/*
Package protoexample is a generated protocol buffer package.
It is generated from these files:
test.proto
It has these top-level messages:
Test
*/
package protoexample
import proto "github.com/golang/protobuf/proto"
import math "math"
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = math.Inf
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type FOO int32
@ -26,88 +26,273 @@ const (
FOO_X FOO = 17
)
var FOO_name = map[int32]string{
// Enum value maps for FOO.
var (
FOO_name = map[int32]string{
17: "X",
}
var FOO_value = map[string]int32{
}
FOO_value = map[string]int32{
"X": 17,
}
}
)
func (x FOO) Enum() *FOO {
p := new(FOO)
*p = x
return p
}
func (x FOO) String() string {
return proto.EnumName(FOO_name, int32(x))
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (x *FOO) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO")
func (FOO) Descriptor() protoreflect.EnumDescriptor {
return file_test_proto_enumTypes[0].Descriptor()
}
func (FOO) Type() protoreflect.EnumType {
return &file_test_proto_enumTypes[0]
}
func (x FOO) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Do not use.
func (x *FOO) UnmarshalJSON(b []byte) error {
num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
if err != nil {
return err
}
*x = FOO(value)
*x = FOO(num)
return nil
}
// Deprecated: Use FOO.Descriptor instead.
func (FOO) EnumDescriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0}
}
type Test struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
XXX_unrecognized []byte `json:"-"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup,json=optionalgroup" json:"optionalgroup,omitempty"`
}
// Default values for Test fields.
const (
Default_Test_Type = int32(77)
)
func (x *Test) Reset() {
*x = Test{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Test) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (m *Test) Reset() { *m = Test{} }
func (m *Test) String() string { return proto.CompactTextString(m) }
func (*Test) ProtoMessage() {}
const Default_Test_Type int32 = 77
func (x *Test) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
func (m *Test) GetLabel() string {
if m != nil && m.Label != nil {
return *m.Label
// Deprecated: Use Test.ProtoReflect.Descriptor instead.
func (*Test) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0}
}
func (x *Test) GetLabel() string {
if x != nil && x.Label != nil {
return *x.Label
}
return ""
}
func (m *Test) GetType() int32 {
if m != nil && m.Type != nil {
return *m.Type
func (x *Test) GetType() int32 {
if x != nil && x.Type != nil {
return *x.Type
}
return Default_Test_Type
}
func (m *Test) GetReps() []int64 {
if m != nil {
return m.Reps
func (x *Test) GetReps() []int64 {
if x != nil {
return x.Reps
}
return nil
}
func (m *Test) GetOptionalgroup() *Test_OptionalGroup {
if m != nil {
return m.Optionalgroup
func (x *Test) GetOptionalgroup() *Test_OptionalGroup {
if x != nil {
return x.Optionalgroup
}
return nil
}
type Test_OptionalGroup struct {
RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"`
XXX_unrecognized []byte `json:"-"`
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RequiredField *string `protobuf:"bytes,5,req,name=RequiredField" json:"RequiredField,omitempty"`
}
func (x *Test_OptionalGroup) Reset() {
*x = Test_OptionalGroup{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Test_OptionalGroup) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} }
func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) }
func (*Test_OptionalGroup) ProtoMessage() {}
func (m *Test_OptionalGroup) GetRequiredField() string {
if m != nil && m.RequiredField != nil {
return *m.RequiredField
func (x *Test_OptionalGroup) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Test_OptionalGroup.ProtoReflect.Descriptor instead.
func (*Test_OptionalGroup) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Test_OptionalGroup) GetRequiredField() string {
if x != nil && x.RequiredField != nil {
return *x.RequiredField
}
return ""
}
func init() {
proto.RegisterEnum("protoexample.FOO", FOO_name, FOO_value)
var File_test_proto protoreflect.FileDescriptor
var file_test_proto_rawDesc = []byte{
0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x04, 0x54,
0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x02,
0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x3a, 0x02, 0x37, 0x37, 0x52, 0x04, 0x74, 0x79, 0x70,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52,
0x04, 0x72, 0x65, 0x70, 0x73, 0x12, 0x46, 0x0a, 0x0d, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61,
0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0a, 0x32, 0x20, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x54, 0x65, 0x73, 0x74,
0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d,
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x1a, 0x35, 0x0a,
0x0d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x24,
0x0a, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18,
0x05, 0x20, 0x02, 0x28, 0x09, 0x52, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46,
0x69, 0x65, 0x6c, 0x64, 0x2a, 0x0c, 0x0a, 0x03, 0x46, 0x4f, 0x4f, 0x12, 0x05, 0x0a, 0x01, 0x58,
0x10, 0x11,
}
var (
file_test_proto_rawDescOnce sync.Once
file_test_proto_rawDescData = file_test_proto_rawDesc
)
func file_test_proto_rawDescGZIP() []byte {
file_test_proto_rawDescOnce.Do(func() {
file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData)
})
return file_test_proto_rawDescData
}
var file_test_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_test_proto_goTypes = []any{
(FOO)(0), // 0: protoexample.FOO
(*Test)(nil), // 1: protoexample.Test
(*Test_OptionalGroup)(nil), // 2: protoexample.Test.OptionalGroup
}
var file_test_proto_depIdxs = []int32{
2, // 0: protoexample.Test.optionalgroup:type_name -> protoexample.Test.OptionalGroup
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_test_proto_init() }
func file_test_proto_init() {
if File_test_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_test_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Test); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_test_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*Test_OptionalGroup); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_test_proto_rawDesc,
NumEnums: 1,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_test_proto_goTypes,
DependencyIndexes: file_test_proto_depIdxs,
EnumInfos: file_test_proto_enumTypes,
MessageInfos: file_test_proto_msgTypes,
}.Build()
File_test_proto = out.File
file_test_proto_rawDesc = nil
file_test_proto_goTypes = nil
file_test_proto_depIdxs = nil
}

612
tree.go
View File

@ -5,9 +5,19 @@
package gin
import (
"bytes"
"net/url"
"strings"
"unicode"
"unicode/utf8"
"github.com/gin-gonic/gin/internal/bytesconv"
)
var (
strColon = []byte(":")
strStar = []byte("*")
strSlash = []byte("/")
)
// Param is a single URL parameter, consisting of a key and a value.
@ -21,8 +31,8 @@ type Param struct {
// It is therefore safe to read values by the index.
type Params []Param
// Get returns the value of the first Param which key matches the given name.
// If no matching Param is found, an empty string is returned.
// Get returns the value of the first Param which key matches the given name and a boolean true.
// If no matching Param is found, an empty string is returned and a boolean false .
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
@ -71,23 +81,33 @@ func longestCommonPrefix(a, b string) int {
return i
}
func countParams(path string) uint8 {
var n uint
for i := 0; i < len(path); i++ {
if path[i] == ':' || path[i] == '*' {
n++
// addChild will add a child node, keeping wildcardChild at the end
func (n *node) addChild(child *node) {
if n.wildChild && len(n.children) > 0 {
wildcardChild := n.children[len(n.children)-1]
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
} else {
n.children = append(n.children, child)
}
}
if n >= 255 {
return 255
}
return uint8(n)
}
func countParams(path string) uint16 {
var n uint16
s := bytesconv.StringToBytes(path)
n += uint16(bytes.Count(s, strColon))
n += uint16(bytes.Count(s, strStar))
return n
}
func countSections(path string) uint16 {
s := bytesconv.StringToBytes(path)
return uint16(bytes.Count(s, strSlash))
}
type nodeType uint8
const (
static nodeType = iota // default
static nodeType = iota
root
param
catchAll
@ -96,16 +116,15 @@ const (
type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
nType nodeType
priority uint32
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}
// increments priority of the given child and reorders if necessary.
// Increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(pos int) int {
cs := n.children
cs[pos].priority++
@ -118,11 +137,11 @@ func (n *node) incrementChildPrio(pos int) int {
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// build new index char string
// Build new index char string
if newPos != pos {
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
n.indices[pos:pos+1] + // the index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
n.indices[pos:pos+1] + // The index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
}
return newPos
@ -133,11 +152,10 @@ func (n *node) incrementChildPrio(pos int) int {
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
numParams := countParams(path)
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(numParams, path, fullPath, handlers)
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
@ -146,11 +164,6 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
walk:
for {
// Update maxParams of the current node
if numParams > n.maxParams {
n.maxParams = numParams
}
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
@ -161,6 +174,7 @@ walk:
child := node{
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handlers: n.handlers,
@ -168,16 +182,9 @@ walk:
fullPath: n.fullPath,
}
// Update maxParams (max of all children)
for _, v := range child.children {
if v.maxParams > child.maxParams {
child.maxParams = v.maxParams
}
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]})
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
@ -187,41 +194,9 @@ walk:
// Make new node a child of this node
if i < len(path) {
path = path[i:]
if n.wildChild {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
// check for longer wildcard, e.g. :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue walk
}
}
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
c := path[0]
// slash after param
// '/' after param
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
@ -240,32 +215,58 @@ walk:
}
// Otherwise insert it
if c != ':' && c != '*' {
if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
maxParams: numParams,
fullPath: fullPath,
}
n.children = append(n.children, child)
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
} else if n.wildChild {
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
n = n.children[len(n.children)-1]
n.priority++
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Adding a child to a catchAll is not possible
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
n.insertChild(numParams, path, fullPath, handlers)
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
n.insertChild(path, fullPath, handlers)
return
}
// Otherwise and handle to current node
// Otherwise add handle to current node
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
n.fullPath = fullPath
return
}
}
// Search for a wildcard segment and check the name for invalid characters.
// Returns -1 as index, if no wildcard war found.
// Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start
for start, c := range []byte(path) {
@ -289,15 +290,15 @@ func findWildcard(path string) (wildcard string, i int, valid bool) {
return "", -1, false
}
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
for numParams > 0 {
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// Find prefix until first wildcard
wildcard, i, valid := findWildcard(path)
if i < 0 { // No wildcard found
break
}
// The wildcard name must not contain ':' and '*'
// The wildcard name must only contain one ':' or '*' character
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
@ -308,13 +309,6 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
// Check if this node has existing children which would be
// unreachable if we insert the wildcard here
if len(n.children) > 0 {
panic("wildcard segment '" + wildcard +
"' conflicts with existing children in path '" + fullPath + "'")
}
if wildcard[0] == ':' { // param
if i > 0 {
// Insert prefix before the current wildcard
@ -322,29 +316,26 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
path = path[i:]
}
n.wildChild = true
child := &node{
nType: param,
path: wildcard,
maxParams: numParams,
fullPath: fullPath,
}
n.children = []*node{child}
n.addChild(child)
n.wildChild = true
n = child
n.priority++
numParams--
// if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/'
// will be another subpath starting with '/'
if len(wildcard) < len(path) {
path = path[len(wildcard):]
child := &node{
maxParams: numParams,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n.addChild(child)
n = child
continue
}
@ -355,12 +346,17 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
}
// catchAll
if i+len(wildcard) != len(path) || numParams > 1 {
if i+len(wildcard) != len(path) {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]
panic("catch-all wildcard '" + path +
"' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg +
"' in existing prefix '" + n.path + pathSeg +
"'")
}
// currently fixed width 1 for '/'
@ -375,14 +371,10 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
fullPath: fullPath,
}
// update maxParams of the parent node
if n.maxParams < 1 {
n.maxParams = 1
}
n.children = []*node{child}
n.addChild(child)
n.indices = string('/')
n = child
n.priority++
@ -391,7 +383,6 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handlers: handlers,
priority: 1,
fullPath: fullPath,
@ -410,64 +401,79 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// nodeValue holds return values of (*Node).getValue method
type nodeValue struct {
handlers HandlersChain
params Params
params *Params
tsr bool
fullPath string
}
// getValue returns the handle registered with the given path (key). The values of
type skippedNode struct {
path string
node *node
paramsCount int16
}
// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
value.params = po
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
var globalParamsCount int16
walk: // Outer loop for walking the tree
for {
prefix := n.path
if path == prefix {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true
return
}
// No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
indices := n.indices
for i, max := 0, len(indices); i < max; i++ {
if indices[i] == '/' {
n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil)
return
}
}
return
}
if len(path) > len(prefix) && path[:len(prefix)] == prefix {
if len(path) > len(prefix) {
if path[:len(prefix)] == prefix {
path = path[len(prefix):]
// If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue
// to walk down the tree
if !n.wildChild {
c := path[0]
indices := n.indices
for i, max := 0, len(indices); i < max; i++ {
if c == indices[i] {
// Try all the non-wildcard children first by matching the indices
idxc := path[0]
for i, c := range []byte(n.indices) {
if c == idxc {
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
if n.wildChild {
index := len(*skippedNodes)
*skippedNodes = (*skippedNodes)[:index+1]
(*skippedNodes)[index] = skippedNode{
path: prefix + path,
node: &node{
path: n.path,
wildChild: n.wildChild,
nType: n.nType,
priority: n.priority,
children: n.children,
handlers: n.handlers,
fullPath: n.fullPath,
},
paramsCount: globalParamsCount,
}
}
n = n.children[i]
continue walk
}
}
if !n.wildChild {
// If the path at the end of the loop is not equal to '/' and the current node has no child nodes
// the current node needs to roll back to last valid skippedNode
if path != "/" {
for length := len(*skippedNodes); length > 0; length-- {
skippedNode := (*skippedNodes)[length-1]
*skippedNodes = (*skippedNodes)[:length-1]
if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path
n = skippedNode.node
if value.params != nil {
*value.params = (*value.params)[:skippedNode.paramsCount]
}
globalParamsCount = skippedNode.paramsCount
continue walk
}
}
}
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
@ -475,31 +481,39 @@ walk: // Outer loop for walking the tree
return
}
// handle wildcard child
n = n.children[0]
// Handle wildcard child, which is always at the end of the array
n = n.children[len(n.children)-1]
globalParamsCount++
switch n.nType {
case param:
// find param end (either '/' or path end)
// fix truncate the parameter
// tree_test.go line: 204
// Find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// save param value
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
// Save param value
if params != nil && cap(*params) > 0 {
if value.params == nil {
value.params = params
}
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[1:]
// Expand slice within preallocated capacity
i := len(*value.params)
*value.params = (*value.params)[:i+1]
val := path[:end]
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
value.params[i].Value = val // fallback, in case of error
if v, err := url.QueryUnescape(val); err == nil {
val = v
}
}
(*value.params)[i] = Param{
Key: n.path[1:],
Value: val,
}
} else {
value.params[i].Value = val
}
// we need to go deeper!
@ -523,25 +537,29 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
value.tsr = n.path == "/" && n.handlers != nil
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
}
return
case catchAll:
// save param value
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
// Save param value
if params != nil {
if value.params == nil {
value.params = params
}
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[2:]
// Expand slice within preallocated capacity
i := len(*value.params)
*value.params = (*value.params)[:i+1]
val := path
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
value.params[i].Value = path // fallback, in case of error
if v, err := url.QueryUnescape(path); err == nil {
val = v
}
}
(*value.params)[i] = Param{
Key: n.path[2:],
Value: val,
}
} else {
value.params[i].Value = path
}
value.handlers = n.handlers
@ -552,72 +570,245 @@ walk: // Outer loop for walking the tree
panic("invalid node type")
}
}
}
if path == prefix {
// If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
// the current node needs to roll back to last valid skippedNode
if n.handlers == nil && path != "/" {
for length := len(*skippedNodes); length > 0; length-- {
skippedNode := (*skippedNodes)[length-1]
*skippedNodes = (*skippedNodes)[:length-1]
if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path
n = skippedNode.node
if value.params != nil {
*value.params = (*value.params)[:skippedNode.paramsCount]
}
globalParamsCount = skippedNode.paramsCount
continue walk
}
}
// n = latestNode.children[len(latestNode.children)-1]
}
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
// If there is no handle for this route, but this route has a
// wildcard child, there must be a handle for this path with an
// additional trailing slash
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true
return
}
if path == "/" && n.nType == static {
value.tsr = true
return
}
// No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
for i, c := range []byte(n.indices) {
if c == '/' {
n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil)
return
}
}
return
}
// Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
value.tsr = (path == "/") ||
value.tsr = path == "/" ||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
path == prefix[:len(prefix)-1] && n.handlers != nil)
// roll back to last valid skippedNode
if !value.tsr && path != "/" {
for length := len(*skippedNodes); length > 0; length-- {
skippedNode := (*skippedNodes)[length-1]
*skippedNodes = (*skippedNodes)[:length-1]
if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path
n = skippedNode.node
if value.params != nil {
*value.params = (*value.params)[:skippedNode.paramsCount]
}
globalParamsCount = skippedNode.paramsCount
continue walk
}
}
}
return
}
}
// findCaseInsensitivePath makes a case-insensitive lookup of the given path and tries to find a handler.
// Makes a case-insensitive lookup of the given path and tries to find a handler.
// It can optionally also fix trailing slashes.
// It returns the case-corrected path and a bool indicating whether the lookup
// was successful.
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
const stackBufSize = 128
// Outer loop for walking the tree
for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) {
path = path[len(n.path):]
// Use a static sized buffer on the stack in the common case.
// If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize)
if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, length)
}
ciPath := n.findCaseInsensitivePathRec(
path,
buf, // Preallocate enough memory for new path
[4]byte{}, // Empty rune buffer
fixTrailingSlash,
)
return ciPath, ciPath != nil
}
// Shift bytes in array by n bytes left
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
switch n {
case 0:
return rb
case 1:
return [4]byte{rb[1], rb[2], rb[3], 0}
case 2:
return [4]byte{rb[2], rb[3]}
case 3:
return [4]byte{rb[3]}
default:
return [4]byte{}
}
}
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
npLen := len(n.path)
walk: // Outer loop for walking the tree
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
// Add common prefix to result
oldPath := path
path = path[npLen:]
ciPath = append(ciPath, n.path...)
if len(path) == 0 {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
if n.handlers != nil {
return ciPath, true
return ciPath
}
// No handle found.
// Try to fix the path by adding a trailing slash
if fixTrailingSlash {
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
for i, c := range []byte(n.indices) {
if c == '/' {
n = n.children[i]
if (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) {
return append(ciPath, '/'), true
return append(ciPath, '/')
}
return
return nil
}
}
}
return
return nil
}
// If this node does not have a wildcard (param or catchAll) child,
// we can just look up the next child node and continue to walk down
// the tree
if !n.wildChild {
r := unicode.ToLower(rune(path[0]))
for i, index := range n.indices {
// must use recursive approach since both index and
// ToLower(index) could exist. We must check both.
if r == unicode.ToLower(index) {
out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
if found {
return append(ciPath, out...), true
// Skip rune bytes already processed
rb = shiftNRuneBytes(rb, npLen)
if rb[0] != 0 {
// Old rune not finished
idxc := rb[0]
for i, c := range []byte(n.indices) {
if c == idxc {
// continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
} else {
// Process a new rune
var rv rune
// Find rune start.
// Runes are up to 4 byte long,
// -4 would definitely be another rune.
var off int
for max := min(npLen, 3); off < max; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
break
}
}
// Calculate lowercase bytes of current rune
lo := unicode.ToLower(rv)
utf8.EncodeRune(rb[:], lo)
// Skip already processed bytes
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Lowercase matches
if c == idxc {
// must use a recursive approach since both the
// uppercase byte and the lowercase byte might exist
// as an index
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
// If we found no match, the same for the uppercase rune,
// if it differs
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
// Uppercase matches
if c == idxc {
// Continue with child node
n = n.children[i]
npLen = len(n.path)
continue walk
}
}
}
}
// Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path
found = fixTrailingSlash && path == "/" && n.handlers != nil
return
if fixTrailingSlash && path == "/" && n.handlers != nil {
return ciPath
}
return nil
}
n = n.children[0]
@ -629,39 +820,43 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
end++
}
// add param value to case insensitive path
// Add param value to case insensitive path
ciPath = append(ciPath, path[:end]...)
// we need to go deeper!
// We need to go deeper!
if end < len(path) {
if len(n.children) > 0 {
path = path[end:]
// Continue with child node
n = n.children[0]
npLen = len(n.path)
path = path[end:]
continue
}
// ... but we can't
if fixTrailingSlash && len(path) == end+1 {
return ciPath, true
return ciPath
}
return
return nil
}
if n.handlers != nil {
return ciPath, true
return ciPath
}
if fixTrailingSlash && len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists
n = n.children[0]
if n.path == "/" && n.handlers != nil {
return append(ciPath, '/'), true
return append(ciPath, '/')
}
}
return
return nil
case catchAll:
return append(ciPath, path...), true
return append(ciPath, path...)
default:
panic("invalid node type")
@ -672,13 +867,12 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// Try to fix the path by adding / removing a trailing slash
if fixTrailingSlash {
if path == "/" {
return ciPath, true
return ciPath
}
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
strings.EqualFold(path, n.path[:len(path)]) &&
n.handlers != nil {
return append(ciPath, n.path...), true
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
return append(ciPath, n.path...)
}
}
return
return nil
}

View File

@ -28,6 +28,16 @@ type testRequests []struct {
ps Params
}
func getParams() *Params {
ps := make(Params, 0, 20)
return &ps
}
func getSkippedNodes() *[]skippedNode {
ps := make([]skippedNode, 0, 20)
return &ps
}
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false
if len(unescapes) >= 1 {
@ -35,7 +45,7 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
for _, request := range requests {
value := tree.getValue(request.path, nil, unescape)
value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape)
if value.handlers == nil {
if !request.nilHandler {
@ -50,10 +60,13 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
}
}
if !reflect.DeepEqual(value.params, request.ps) {
if value.params != nil {
if !reflect.DeepEqual(*value.params, request.ps) {
t.Errorf("Params mismatch for route '%s'", request.path)
}
}
}
}
func checkPriorities(t *testing.T, n *node) uint32 {
@ -76,33 +89,11 @@ func checkPriorities(t *testing.T, n *node) uint32 {
return prio
}
func checkMaxParams(t *testing.T, n *node) uint8 {
var maxParams uint8
for i := range n.children {
params := checkMaxParams(t, n.children[i])
if params > maxParams {
maxParams = params
}
}
if n.nType > root && !n.wildChild {
maxParams++
}
if n.maxParams != maxParams {
t.Errorf(
"maxParams mismatch for node '%s': is %d, should be %d",
n.path, n.maxParams, maxParams,
)
}
return maxParams
}
func TestCountParams(t *testing.T) {
if countParams("/path/:param1/static/*catch-all") != 2 {
t.Fail()
}
if countParams(strings.Repeat("/:param", 256)) != 255 {
if countParams(strings.Repeat("/:param", 256)) != 256 {
t.Fail()
}
}
@ -142,7 +133,6 @@ func TestTreeAddAndGet(t *testing.T) {
})
checkPriorities(t, tree)
checkMaxParams(t, tree)
}
func TestTreeWildcard(t *testing.T) {
@ -150,11 +140,16 @@ func TestTreeWildcard(t *testing.T) {
routes := [...]string{
"/",
"/cmd/:tool/:sub",
"/cmd/:tool/",
"/cmd/:tool/:sub",
"/cmd/whoami",
"/cmd/whoami/root",
"/cmd/whoami/root/",
"/src/*filepath",
"/search/",
"/search/:query",
"/search/gin-gonic",
"/search/google",
"/user_:name",
"/user_:name/about",
"/files/:dir/*filepath",
@ -163,6 +158,40 @@ func TestTreeWildcard(t *testing.T) {
"/doc/go1.html",
"/info/:user/public",
"/info/:user/project/:project",
"/info/:user/project/golang",
"/aa/*xx",
"/ab/*xx",
"/:cc",
"/c1/:dd/e",
"/c1/:dd/e1",
"/:cc/cc",
"/:cc/:dd/ee",
"/:cc/:dd/:ee/ff",
"/:cc/:dd/:ee/:ff/gg",
"/:cc/:dd/:ee/:ff/:gg/hh",
"/get/test/abc/",
"/get/:param/abc/",
"/something/:paramname/thirdthing",
"/something/secondthing/test",
"/get/abc",
"/get/:param",
"/get/abc/123abc",
"/get/abc/:param",
"/get/abc/123abc/xxx8",
"/get/abc/123abc/:param",
"/get/abc/123abc/xxx8/1234",
"/get/abc/123abc/xxx8/:param",
"/get/abc/123abc/xxx8/1234/ffas",
"/get/abc/123abc/xxx8/1234/:param",
"/get/abc/123abc/xxx8/1234/kkdd/12c",
"/get/abc/123abc/xxx8/1234/kkdd/:param",
"/get/abc/:param/test",
"/get/abc/123abd/:param",
"/get/abc/123abddd/:param",
"/get/abc/123/:param",
"/get/abc/123abg/:param",
"/get/abc/123abf/:param",
"/get/abc/123abfff/:param",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
@ -170,23 +199,125 @@ func TestTreeWildcard(t *testing.T) {
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
{"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
{"/cmd/whoami", false, "/cmd/whoami", nil},
{"/cmd/whoami/", true, "/cmd/whoami", nil},
{"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
{"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
{"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
{"/search/gin-gonic", false, "/search/gin-gonic", nil},
{"/search/google", false, "/search/google", nil},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
// * Error with argument being intercepted
// new PR handle (/all /all/cc /a/cc)
// fix PR: https://github.com/gin-gonic/gin/pull/2796
{"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
{"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
{"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
{"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
{"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
{"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
{"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
{"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
{"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
{"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
{"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
{"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
{"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
{"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
{"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
{"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}},
{"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}},
{"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}},
{"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
{"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
{"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
{"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
{"/get/test/abc/", false, "/get/test/abc/", nil},
{"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
{"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
{"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
{"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
{"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
{"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
{"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
{"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
{"/something/secondthing/test", false, "/something/secondthing/test", nil},
{"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
{"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
{"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
{"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
{"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
{"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
{"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
{"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
{"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
{"/get/abc", false, "/get/abc", nil},
{"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
{"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
{"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
{"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
{"/get/abc/123abc", false, "/get/abc/123abc", nil},
{"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
{"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
{"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
{"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
{"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
{"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
{"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
{"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
{"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
{"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
{"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
{"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
{"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
{"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
{"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
{"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
{"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
{"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
{"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
{"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
{"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
{"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
{"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
{"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
{"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
{"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
{"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
{"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
})
checkPriorities(t, tree)
checkMaxParams(t, tree)
}
func TestUnescapeParameters(t *testing.T) {
@ -224,10 +355,9 @@ func TestUnescapeParameters(t *testing.T) {
}, unescape)
checkPriorities(t, tree)
checkMaxParams(t, tree)
}
func catchPanic(testFunc func()) (recv interface{}) {
func catchPanic(testFunc func()) (recv any) {
defer func() {
recv = recover()
}()
@ -262,20 +392,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
func TestTreeWildcardConflict(t *testing.T) {
routes := []testRoute{
{"/cmd/:tool/:sub", false},
{"/cmd/vet", true},
{"/cmd/vet", false},
{"/foo/bar", false},
{"/foo/:name", false},
{"/foo/:names", true},
{"/cmd/*path", true},
{"/cmd/:badvar", true},
{"/cmd/:tool/names", false},
{"/cmd/:tool/:badsub/details", true},
{"/src/*filepath", false},
{"/src/:file", true},
{"/src/static.json", true},
{"/src/*filepathx", true},
{"/src/", true},
{"/src/foo/bar", true},
{"/src1/", false},
{"/src1/*filepath", true},
{"/src2*filepath", true},
{"/src2/*filepath", false},
{"/search/:query", false},
{"/search/invalid", true},
{"/search/valid", false},
{"/user_:name", false},
{"/user_x", true},
{"/user_x", false},
{"/user_:name", false},
{"/id:id", false},
{"/id/:id", true},
{"/id/:id", false},
}
testRoutes(t, routes)
}
func TestCatchAllAfterSlash(t *testing.T) {
routes := []testRoute{
{"/non-leading-*catchall", true},
}
testRoutes(t, routes)
}
@ -283,20 +431,23 @@ func TestTreeWildcardConflict(t *testing.T) {
func TestTreeChildConflict(t *testing.T) {
routes := []testRoute{
{"/cmd/vet", false},
{"/cmd/:tool/:sub", true},
{"/cmd/:tool", false},
{"/cmd/:tool/:sub", false},
{"/cmd/:tool/misc", false},
{"/cmd/:tool/:othersub", true},
{"/src/AUTHORS", false},
{"/src/*filepath", true},
{"/user_x", false},
{"/user_:name", true},
{"/user_:name", false},
{"/id/:id", false},
{"/id:id", true},
{"/:id", true},
{"/id:id", false},
{"/:id", false},
{"/*filepath", true},
}
testRoutes(t, routes)
}
func TestTreeDupliatePath(t *testing.T) {
func TestTreeDuplicatePath(t *testing.T) {
tree := &node{}
routes := [...]string{
@ -323,12 +474,14 @@ func TestTreeDupliatePath(t *testing.T) {
}
}
//printChildren(tree, "")
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/doc/", false, "/doc/", nil},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
})
}
@ -356,6 +509,8 @@ func TestTreeCatchAllConflict(t *testing.T) {
{"/src/*filepath/x", true},
{"/src2/", false},
{"/src2/*filepath/x", true},
{"/src3/*filepath", false},
{"/src3/*filepath/x", true},
}
testRoutes(t, routes)
}
@ -372,7 +527,6 @@ func TestTreeCatchMaxParams(t *testing.T) {
tree := &node{}
var route = "/cmd/*filepath"
tree.addRoute(route, fakeHandler(route))
checkMaxParams(t, tree)
}
func TestTreeDoubleWildcard(t *testing.T) {
@ -433,7 +587,15 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/go1.html",
"/no/a",
"/no/b",
"/api/hello/:name",
"/api/:page/:name",
"/api/hello/:name/bar/",
"/api/bar/:name",
"/api/baz/foo",
"/api/baz/foo/bar",
"/blog/:p",
"/posts/:b/:c",
"/posts/b/:c/d/",
"/vendor/:x/*y",
}
for _, route := range routes {
recv := catchPanic(func() {
@ -459,9 +621,22 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/admin/config/",
"/admin/config/permissions/",
"/doc/",
"/admin/static/",
"/admin/cfg/",
"/admin/cfg/users/",
"/api/hello/x/bar",
"/api/baz/foo/",
"/api/baz/bax/",
"/api/bar/huh/",
"/api/baz/foo/bar/",
"/api/world/abc/",
"/blog/pp/",
"/posts/b/c/d",
"/vendor/x",
}
for _, route := range tsrRoutes {
value := tree.getValue(route, nil, false)
value := tree.getValue(route, nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !value.tsr {
@ -475,10 +650,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/no/",
"/_",
"/_/",
"/api/world/abc",
"/api",
"/api/",
"/api/hello/x/foo",
"/api/baz/foo/bad",
"/foo/p/p",
}
for _, route := range noTsrRoutes {
value := tree.getValue(route, nil, false)
value := tree.getValue(route, nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if value.tsr {
@ -497,7 +676,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv)
}
value := tree.getValue("/", nil, false)
value := tree.getValue("/", nil, getSkippedNodes(), false)
if value.handlers != nil {
t.Fatalf("non-nil handler")
} else if value.tsr {
@ -505,9 +684,32 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
}
}
func TestRedirectTrailingSlash(t *testing.T) {
var data = []struct {
path string
}{
{"/hello/:name"},
{"/hello/:name/123"},
{"/hello/:name/234"},
}
node := &node{}
for _, item := range data {
node.addRoute(item.path, fakeHandler("test"))
}
value := node.getValue("/hello/abx/", nil, getSkippedNodes(), false)
if value.tsr != true {
t.Fatalf("want true, is false")
}
}
func TestTreeFindCaseInsensitivePath(t *testing.T) {
tree := &node{}
longPath := "/l" + strings.Repeat("o", 128) + "ng"
lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
routes := [...]string{
"/hi",
"/b/",
@ -531,6 +733,17 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
"/doc/go/away",
"/no/a",
"/no/b",
"/Π",
"/u/apfêl/",
"/u/äpfêl/",
"/u/öpfêl",
"/v/Äpfêl/",
"/v/Öpfêl",
"/w/♬", // 3 byte
"/w/♭/", // 3 byte, last byte differs
"/w/𠜎", // 4 byte
"/w/𠜏/", // 4 byte
longPath,
}
for _, route := range routes {
@ -609,6 +822,21 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
{"/DOC/", "/doc", true, true},
{"/NO", "", false, true},
{"/DOC/GO", "", false, true},
{"/π", "/Π", true, false},
{"/π/", "/Π", true, true},
{"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
{"/u/ÄPFÊL", "/u/äpfêl/", true, true},
{"/u/ÖPFÊL/", "/u/öpfêl", true, true},
{"/u/ÖPFÊL", "/u/öpfêl", true, false},
{"/v/äpfêL/", "/v/Äpfêl/", true, false},
{"/v/äpfêL", "/v/Äpfêl/", true, true},
{"/v/öpfêL/", "/v/Öpfêl", true, true},
{"/v/öpfêL", "/v/Öpfêl", true, false},
{"/w/♬/", "/w/♬", true, true},
{"/w/♭", "/w/♭/", true, true},
{"/w/𠜎/", "/w/𠜎", true, true},
{"/w/𠜏", "/w/𠜏/", true, true},
{lOngPath, longPath, true, true},
}
// With fixTrailingSlash = true
for _, test := range tests {
@ -648,7 +876,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
// normal lookup
recv := catchPanic(func() {
tree.getValue("/test", nil, false)
tree.getValue("/test", nil, getSkippedNodes(), false)
})
if rs, ok := recv.(string); !ok || rs != panicMsg {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
@ -663,6 +891,19 @@ func TestTreeInvalidNodeType(t *testing.T) {
}
}
func TestTreeInvalidParamsType(t *testing.T) {
tree := &node{}
tree.wildChild = true
tree.children = append(tree.children, &node{})
tree.children[0].nType = 2
// set invalid Params type
params := make(Params, 0)
// try to trigger slice bounds out of range with capacity 0
tree.getValue("/test", &params, getSkippedNodes(), false)
}
func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct {
route string
@ -673,8 +914,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
{"/conxxx", "xxx", `/con:tact`, `:tact`},
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
{"/con:nection", ":nection", `/con:tact`, `:tact`},
}
for _, conflict := range conflicts {
@ -696,8 +936,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
tree.addRoute(conflict.route, fakeHandler(conflict.route))
})
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'",
conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
t.Fatalf("invalid wildcard conflict error (%v)", recv)
}
}

View File

@ -12,13 +12,14 @@ import (
"reflect"
"runtime"
"strings"
"unicode"
)
// BindKey indicates a default bind key.
const BindKey = "_gin-gonic/gin/bindkey"
// Bind is a helper function for given interface object and returns a Gin middleware.
func Bind(val interface{}) HandlerFunc {
func Bind(val any) HandlerFunc {
value := reflect.ValueOf(val)
if value.Kind() == reflect.Ptr {
panic(`Bind struct can not be a pointer. Example:
@ -49,8 +50,8 @@ func WrapH(h http.Handler) HandlerFunc {
}
}
// H is a shortcut for map[string]interface{}
type H 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.
func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
@ -89,21 +90,24 @@ func filterFlags(content string) string {
return content
}
func chooseData(custom, wildcard interface{}) interface{} {
if custom == nil {
if wildcard == nil {
panic("negotiation config is invalid")
func chooseData(custom, wildcard any) any {
if custom != nil {
return custom
}
if wildcard != nil {
return wildcard
}
return custom
panic("negotiation config is invalid")
}
func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
if i := strings.IndexByte(part, ';'); i > 0 {
part = part[:i]
}
if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
}
@ -117,7 +121,7 @@ func lastChar(str string) uint8 {
return str[len(str)-1]
}
func nameOfFunction(f interface{}) string {
func nameOfFunction(f any) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}
@ -127,8 +131,7 @@ func joinPaths(absolutePath, relativePath string) string {
}
finalPath := path.Join(absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
if appendSlash {
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
@ -149,3 +152,13 @@ func resolveAddress(addr []string) string {
panic("too many parameters")
}
}
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

View File

@ -18,6 +18,12 @@ func init() {
SetMode(TestMode)
}
func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
}
}
type testStruct struct {
T *testing.T
}
@ -39,11 +45,11 @@ func TestWrap(t *testing.T) {
fmt.Fprint(w, "hola!")
}))
w := performRequest(router, "POST", "/path")
w := PerformRequest(router, "POST", "/path")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hello", w.Body.String())
w = performRequest(router, "GET", "/path2")
w = PerformRequest(router, "GET", "/path2")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "hola!", w.Body.String())
}
@ -113,13 +119,13 @@ func TestBindMiddleware(t *testing.T) {
called = true
value = c.MustGet(BindKey).(*bindTestStruct)
})
performRequest(router, "GET", "/?foo=hola&bar=10")
PerformRequest(router, "GET", "/?foo=hola&bar=10")
assert.True(t, called)
assert.Equal(t, "hola", value.Foo)
assert.Equal(t, 10, value.Bar)
called = false
performRequest(router, "GET", "/?foo=hola&bar=1")
PerformRequest(router, "GET", "/?foo=hola&bar=1")
assert.False(t, called)
assert.Panics(t, func() {
@ -137,3 +143,8 @@ func TestMarshalXMLforH(t *testing.T) {
e := h.MarshalXML(enc, x)
assert.Error(t, e)
}
func TestIsASCII(t *testing.T) {
assert.Equal(t, isASCII("test"), true)
assert.Equal(t, isASCII("🧡💛💚💙💜"), false)
}

View File

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