Merge branch 'master' into master

This commit is contained in:
Bo-Yi Wu 2024-05-08 08:54:13 +08:00 committed by GitHub
commit 2478256eda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
114 changed files with 11507 additions and 5168 deletions

View File

@ -3,11 +3,47 @@
- Please provide source code and commit sha if you found a bug. - Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them. - Review existing issues and provide feedback or react to them.
## Description
<!-- Description of a problem -->
## How to reproduce
<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/hello/:name", func(c *gin.Context) {
c.String(200, "Hello %s", c.Param("name"))
})
g.Run(":9000")
}
```
## Expectations
<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:9000/hello/world
Hello world
```
## Actual result
<!-- Actual result showing the problem -->
```
$ curl -i http://localhost:9000/hello/world
<YOUR RESULT>
```
## Environment
- go version: - go version:
- gin version (or commit ref): - gin version (or commit ref):
- operating system: - operating system:
## Description
## Screenshots

View File

@ -1,7 +1,7 @@
- With pull requests: - With pull requests:
- Open your pull request against `master` - Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them. - 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 integrations 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. - 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. - 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@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# 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@v3

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

@ -0,0 +1,82 @@
name: Run Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
check-latest: true
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v5
with:
version: v1.56.2
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ["1.18", "1.19", "1.20", "1.21", "1.22"]
test-tags:
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
- 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@v5
with:
go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/cache@v4
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@v4
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
- name: Format
if: matrix.go-version == '1.22.x'
run: diff -u <(echo -n) <(gofmt -d .)

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

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

4
.gitignore vendored
View File

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

57
.golangci.yml Normal file
View File

@ -0,0 +1,57 @@
run:
timeout: 5m
linters:
enable:
- asciicheck
- 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

56
.goreleaser.yaml Normal file
View File

@ -0,0 +1,56 @@
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.
disable: 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: github
# 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: "Refactor"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Build process updates"
regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
order: 4
- title: "Documentation updates"
regexp: ^.*?docs?(\(.+\))??!?:.+$
order: 4
- title: Others
order: 999

View File

@ -1,44 +0,0 @@
language: go
matrix:
fast_finish: true
include:
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: master
env: GO111MODULE=on
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; else make install; 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 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 ## gin 0.x series authors
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) **Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
------
People and companies, who have contributed, in alphabetical order. People and companies, who have contributed, in alphabetical order.
**@858806258 (杰哥)** - 178inaba <178inaba@users.noreply.github.com>
- Fix typo in example - A. F <hello@clivern.com>
- ABHISHEK SONI <abhishek.rocks26@gmail.com>
- Abhishek Chanda <achanda@users.noreply.github.com>
**@achedeuzot (Klemen Sever)** - Abner Chen <houjunchen@gmail.com>
- Fix newline debug printing - AcoNCodes <acongame@gmail.com>
- Adam Dratwinski <adam.dratwinski@gmail.com>
- Adam Mckaig <adam.mckaig@gmail.com>
**@adammck (Adam Mckaig)** - Adam Zielinski <MusicAdam@users.noreply.github.com>
- Add MIT license - Adonis <donileo@gmail.com>
- Alan Wang <azzwacb9001@126.com>
- Albin Gilles <gilles.albin@gmail.com>
**@AlexanderChen1989 (Alexander)** - Aleksandr Didenko <aa.didenko@yandex.ru>
- Typos in README - Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
- Alex <AWulkan@users.noreply.github.com>
- Alexander <alexanderchenmh@gmail.com>
**@alexanderdidenko (Aleksandr Didenko)** - Alexander Lokhman <alex.lokhman@gmail.com>
- Add support multipart/form-data - Alexander Melentyev <55826637+alexander-melentyev@users.noreply.github.com>
- Alexander Nyquist <nyquist.alexander@gmail.com>
- Allen Ren <kulong0105@gmail.com>
**@alexandernyquist (Alexander Nyquist)** - AllinGo <tanhp@outlook.com>
- Using template.Must to fix multiple return issue - Ammar Bandukwala <ammar@ammar.io>
- ★ Added support for OPTIONS verb - An Xiao (Luffy) <hac@zju.edu.cn>
- ★ Setting response headers before calling WriteHeader - Andre Dublin <81dublin@gmail.com>
- Improved documentation for model binding - Andrew Szeto <github@jabagawee.com>
- ★ Added Content.Redirect() - Andrey Abramov <andreyabramov.aaa@gmail.com>
- ★ Added tons of Unit tests - Andrey Nering <andrey.nering@gmail.com>
- Andrey Smirnov <Smirnov.Andrey@gmail.com>
- Andrii Bubis <firstrow@gmail.com>
**@austinheap (Austin Heap)** - André Bazaglia <bazaglia@users.noreply.github.com>
- Added travis CI integration - Andy Pan <panjf2000@gmail.com>
- Antoine GIRARD <sapk@users.noreply.github.com>
- Anup Kumar Panwar <1anuppanwar@gmail.com>
**@andredublin (Andre Dublin)** - Aravinth Sundaram <gosh.aravind@gmail.com>
- Fix typo in comment - Artem <horechek@gmail.com>
- Ashwani <ashwanisharma686@gmail.com>
- Aurelien Regat-Barrel <arb@cyberkarma.net>
**@bredov (Ludwig Valda Vasquez)** - Austin Heap <me@austinheap.com>
- Fix html templating in debug mode - Barnabus <jbampton@users.noreply.github.com>
- Bo-Yi Wu <appleboy.tw@gmail.com>
- Boris Borshevsky <BorisBorshevsky@gmail.com>
**@bluele (Jun Kimura)** - Boyi Wu <p581581@gmail.com>
- Fixes code examples in README - BradyBromley <51128276+BradyBromley@users.noreply.github.com>
- Brendan Fosberry <brendan@shopkeep.com>
- Brian Wigginton <brianwigginton@gmail.com>
**@chad-russell** - Carlos Eduardo <carlosedp@gmail.com>
- ★ Support for serializing gin.H into XML - Chad Russell <chaddouglasrussell@gmail.com>
- Charles <cxjava@gmail.com>
- Christian Muehlhaeuser <muesli@gmail.com>
**@dickeyxxx (Jeff Dickey)** - Christian Persson <saser@live.se>
- Typos in README - Christopher Harrington <ironiridis@gmail.com>
- Add example about serving static files - Damon Zhao <yijun.zhao@outlook.com>
- Dan Markham <dmarkham@gmail.com>
- Dang Nguyen <hoangdang.me@gmail.com>
**@donileo (Adonis)** - Daniel Krom <kromdan@gmail.com>
- Add NoMethod handler - Daniel M. Lambea <dmlambea@gmail.com>
- Danieliu <liudanking@gmail.com>
- David Irvine <aviddiviner@gmail.com>
**@dutchcoders (DutchCoders)** - David Zhang <crispgm@gmail.com>
- ★ Fix security bug that allows client to spoof ip - Davor Kapsa <dvrkps@users.noreply.github.com>
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate - DeathKing <DeathKing@users.noreply.github.com>
- Dennis Cho <47404603+forest747@users.noreply.github.com>
- Dmitry Dorogin <dmirogin@ya.ru>
**@el3ctro- (Joshua Loper)** - Dmitry Kutakov <vkd.castle@gmail.com>
- Fix typo in example - Dmitry Sedykh <dmitrys@d3h.local>
- Don2Quixote <35610661+Don2Quixote@users.noreply.github.com>
- Donn Pebe <iam@donnpebe.com>
**@ethankan (Ethan Kan)** - Dustin Decker <dustindecker@protonmail.com>
- Unsigned integers in binding - Eason Lin <easonlin404@gmail.com>
- Edward Betts <edward@4angle.com>
- Egor Seredin <4819888+agmt@users.noreply.github.com>
**(Evgeny Persienko)** - Emmanuel Goh <emmanuel@visenze.com>
- Validate sub structures - Equim <sayaka@ekyu.moe>
- Eren A. Akyol <eren@redmc.me>
- Eric_Lee <xplzv@126.com>
**@frankbille (Frank Bille)** - Erik Bender <erik.bender@develerik.dev>
- Add support for HTTP Realm Auth - Ethan Kan <ethankan@neoplot.com>
- Evgeny Persienko <e.persienko@office.ngs.ru>
- Faisal Alam <ifaisalalam@gmail.com>
**@fmd (Fareed Dudhia)** - Fareed Dudhia <fareeddudhia@googlemail.com>
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate - Filip Figiel <figiel.filip@gmail.com>
- Florian Polster <couchpolster@icqmail.com>
- Frank Bille <github@frankbille.dk>
**@ironiridis (Christopher Harrington)** - Franz Bettag <franz@bett.ag>
- Remove old reference - Ganlv <ganlvtech@users.noreply.github.com>
- Gaozhen Ying <yinggaozhen@hotmail.com>
- George Gabolaev <gabolaev98@gmail.com>
**@jammie-stackhouse (Jamie Stackhouse)** - George Kirilenko <necryin@users.noreply.github.com>
- Add more shortcuts for router methods - Georges Varouchas <georges.varouchas@gmail.com>
- Gordon Tyler <gordon@doxxx.net>
- Harindu Perera <harinduenator@gmail.com>
**@jasonrhansen** - Helios <674876158@qq.com>
- Fix spelling and grammar errors in documentation - Henry Kwan <piengeng@users.noreply.github.com>
- Henry Yee <henry@yearning.io>
- Himanshu Mishra <OrkoHunter@users.noreply.github.com>
**@JasonSoft (Jason Lee)** - Hiroyuki Tanaka <h.tanaka.0325@gmail.com>
- Fix typo in comment - Ibraheem Ahmed <ibrah1440@gmail.com>
- Ignacio Galindo <joiggama@gmail.com>
- Igor H. Vieira <zignd.igor@gmail.com>
**@joiggama (Ignacio Galindo)** - Ildar1111 <54001462+Ildar1111@users.noreply.github.com>
- Add utf-8 charset header on renders - Iskander (Alex) Sharipov <iskander.sharipov@intel.com>
- Ismail Gjevori <isgjevori@protonmail.com>
- Ivan Chen <allenivan@gmail.com>
**@julienschmidt (Julien Schmidt)** - JINNOUCHI Yasushi <delphinus@remora.cx>
- gofmt the code examples - James Pettyjohn <japettyjohn@users.noreply.github.com>
- Jamie Stackhouse <jamie.stackhouse@redspace.com>
- Jason Lee <jawc@hotmail.com>
**@kelcecil (Kel Cecil)** - Javier Provecho <j.provecho@dartekstudios.com>
- Fix readme typo - Javier Provecho <javier.provecho@bq.com>
- Javier Provecho <javiertitan@gmail.com>
- Javier Provecho Fernandez <j.provecho@dartekstudios.com>
**@kyledinh (Kyle Dinh)** - Javier Provecho Fernandez <javiertitan@gmail.com>
- Adds RunTLS() - Jean-Christophe Lebreton <jclebreton@gmail.com>
- Jeff <laojianzi1994@gmail.com>
- Jeremy Loy <jeremy.b.loy@icloud.com>
**@LinusU (Linus Unnebäck)** - Jim Filippou <p3160253@aueb.gr>
- Small fixes in README - Jimmy Pettersson <jimmy@expertmaker.com>
- John Bampton <jbampton@users.noreply.github.com>
- Johnny Dallas <johnnydallas0308@gmail.com>
**@loongmxbt (Saint Asky)** - Johnny Dallas <theonlyjohnny@theonlyjohnny.sh>
- Fix typo in example - Jonathan (JC) Chen <jc@dijonkitchen.org>
- Josep Jesus Bigorra Algaba <42377845+averageflow@users.noreply.github.com>
- Josh Horowitz <joshua.m.horowitz@gmail.com>
**@lucas-clemente (Lucas Clemente)** - Joshua Loper <josh.el3@gmail.com>
- ★ work around path.Join removing trailing slashes from routes - Julien Schmidt <github@julienschmidt.com>
- Jun Kimura <jksmphone@gmail.com>
- Justin Beckwith <justin.beckwith@gmail.com>
**@mattn (Yasuhiro Matsumoto)** - Justin Israel <justinisrael@gmail.com>
- Improve color logger - Justin Mayhew <mayhew@live.ca>
- Jérôme Laforge <jerome-laforge@users.noreply.github.com>
- Kacper Bąk <56700396+53jk1@users.noreply.github.com>
**@mdigger (Dmitry Sedykh)** - Kamron Batman <kamronbatman@users.noreply.github.com>
- Fixes Form binding when content-type is x-www-form-urlencoded - Kane Rogers <kane@cleanstream.com.au>
- No repeat call c.Writer.Status() in gin.Logger - Kaushik Neelichetty <kaushikneelichetty6132@gmail.com>
- Fixes Content-Type for json render - Keiji Yoshida <yoshida.keiji.84@gmail.com>
- Kel Cecil <kel.cecil@listhub.com>
- Kevin Mulvey <kmulvey@linux.com>
**@mirzac (Mirza Ceric)** - Kevin Zhu <ipandtcp@gmail.com>
- Fix debug printing - Kirill Motkov <motkov.kirill@gmail.com>
- Klemen Sever <ksever@student.42.fr>
- Kristoffer A. Iversen <kristoffer.a.iversen@gmail.com>
**@mopemope (Yutaka Matsubara)** - Krzysztof Szafrański <k.p.szafranski@gmail.com>
- ★ Adds Godep support (Dependencies Manager) - Kumar McMillan <kumar.mcmillan@gmail.com>
- Fix variadic parameter in the flexible render API - Kyle Mcgill <email@kylescottmcgill.com>
- Fix Corrupted plain render - Lanco <35420416+lancoLiu@users.noreply.github.com>
- Add Pluggable View Renderer Example - Levi Olson <olson.levi@gmail.com>
- Lin Kao-Yuan <mosdeo@gmail.com>
- Linus Unnebäck <linus@folkdatorn.se>
**@msemenistyi (Mykyta Semenistyi)** - Lucas Clemente <lucas@clemente.io>
- update Readme.md. Add code to String method - Ludwig Valda Vasquez <bredov@gmail.com>
- Luis GG <lggomez@users.noreply.github.com>
- MW Lim <williamchange@gmail.com>
**@msoedov (Sasha Myasoedov)** - Maksimov Sergey <konjoot@gmail.com>
- ★ Adds tons of unit tests. - Manjusaka <lizheao940510@gmail.com>
- Manu MA <manu.mtza@gmail.com>
- Manu MA <manu.valladolid@gmail.com>
**@ngerakines (Nick Gerakines)** - Manu Mtz-Almeida <manu.valladolid@gmail.com>
- ★ Improves API, c.GET() doesn't panic - Manu Mtz.-Almeida <manu.valladolid@gmail.com>
- Adds MustGet() method - Manuel Alonso <manuelalonso@invisionapp.com>
- Mara Kim <hacker.root@gmail.com>
- Mario Kostelac <mario@intercom.io>
**@r8k (Rajiv Kilaparti)** - Martin Karlsch <martin@karlsch.com>
- Fix Port usage in README. - Matt Newberry <mnewberry@opentable.com>
- Matt Williams <gh@mattyw.net>
- Matthieu MOREL <mmorel-35@users.noreply.github.com>
**@rayrod2030 (Ray Rodriguez)** - Max Hilbrunner <mhilbrunner@users.noreply.github.com>
- Fix typo in example - Maxime Soulé <btik-git@scoubidou.com>
- MetalBreaker <johnymichelson@gmail.com>
- Michael Puncel <mpuncel@squareup.com>
**@rns** - MichaelDeSteven <51652084+MichaelDeSteven@users.noreply.github.com>
- Fix typo in example - Mike <38686456+icy4ever@users.noreply.github.com>
- Mike Stipicevic <mst@ableton.com>
- Miki Tebeka <miki.tebeka@gmail.com>
**@RobAWilkinson (Robert Wilkinson)** - Miles <MilesLin@users.noreply.github.com>
- Add example of forms and params - Mirza Ceric <mirza.ceric@b2match.com>
- Mykyta Semenistyi <nikeiwe@gmail.com>
- Naoki Takano <honten@tinkermode.com>
**@rogierlommers (Rogier Lommers)** - Ngalim Siregar <ngalim.siregar@gmail.com>
- Add updated static serve example - Ni Hao <supernihaooo@qq.com>
- Nick Gerakines <nick@gerakines.net>
- Nikifor Seryakov <nikandfor@gmail.com>
**@se77en (Damon Zhao)** - Notealot <714804968@qq.com>
- Improve color logging - Olivier Mengué <dolmen@cpan.org>
- Olivier Robardet <orobardet@users.noreply.github.com>
- Pablo Moncada <pablo.moncada@bq.com>
**@silasb (Silas Baronda)** - Pablo Moncada <pmoncadaisla@gmail.com>
- Fixing quotes in README - Panmax <967168@qq.com>
- Peperoncino <2wua4nlyi@gmail.com>
- Philipp Meinen <philipp@bind.ch>
**@SkuliOskarsson (Skuli Oskarsson)** - Pierre Massat <pierre@massat.io>
- Fixes some texts in README II - Qt <golang.chen@gmail.com>
- Quentin ROYER <aydendevg@gmail.com>
- README Bot <35302948+codetriage-readme-bot@users.noreply.github.com>
**@slimmy (Jimmy Pettersson)** - Rafal Zajac <rzajac@gmail.com>
- Added messages for required bindings - Rahul Datta Roy <rahuldroy@users.noreply.github.com>
- Rajiv Kilaparti <rajivk085@gmail.com>
- Raphael Gavache <raphael.gavache@datadoghq.com>
**@smira (Andrey Smirnov)** - Ray Rodriguez <rayrod2030@gmail.com>
- Add support for ignored/unexported fields in binding - Regner Blok-Andersen <shadowdf@gmail.com>
- Remco <remco@dutchcoders.io>
- Rex Lee(李俊) <duguying2008@gmail.com>
**@superalsrk (SRK.Lyu)** - Richard Lee <dlackty@gmail.com>
- Update httprouter godeps - Riverside <wangyb65@gmail.com>
- Robert Wilkinson <wilkinson.robert.a@gmail.com>
- Rogier Lommers <rogier@lommers.org>
**@tebeka (Miki Tebeka)** - Rohan Pai <me@rohanpai.com>
- Use net/http constants instead of numeric values - Romain Beuque <rbeuque74@gmail.com>
- Roman Belyakovsky <ihryamzik@gmail.com>
- Roman Zaynetdinov <627197+zaynetro@users.noreply.github.com>
**@techjanitor** - Roman Zaynetdinov <roman.zaynetdinov@lekane.com>
- Update context.go reserved IPs - Ronald Petty <ronald.petty@rx-m.com>
- Ross Wolf <31489089+rw-access@users.noreply.github.com>
- Roy Lou <roylou@gmail.com>
**@yosssi (Keiji Yoshida)** - Rubi <14269809+codenoid@users.noreply.github.com>
- Fix link in README - Ryan <46182144+ryanker@users.noreply.github.com>
- Ryan J. Yoder <me@ryanjyoder.com>
- SRK.Lyu <superalsrk@gmail.com>
**@yuyabee** - Sai <sairoutine@gmail.com>
- Fixed README - 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,8 +1,398 @@
# Gin ChangeLog
### Gin 1.4.0 ## Gin v1.10.0
### Features
* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
* feat(binding): support override default binding implement (#3514) (@ssfyn)
* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
### Bug fixes
* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
* fix(binding): dereference pointer to struct (#3199) (@echovl)
* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
* fix(engine): fix unit test (#3878) (@flc1125)
* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
* fix(uri): query binding bug (#3236) (@illiafox)
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
### Enhancements
* chore(CI): update release args (#3595) (@qloog)
* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
* chore(refactor): modify interface check way (#3855) (@demoManito)
* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
* chore: refactor CI and update dependencies (#3848) (@appleboy)
* chore: refactor configuration files for better readability (#3951) (@appleboy)
* chore: update GitHub Actions configuration (#3792) (@appleboy)
* chore: update changelog categories and improve documentation (#3917) (@appleboy)
* chore: update dependencies to latest versions (#3694) (@appleboy)
* chore: update external dependencies to latest versions (#3950) (@appleboy)
* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
### Build process updates
* build(codecov): Added a codecov configuration (#3891) (@flc1125)
* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
* ci(testing): add go1.22 version (#3842) (@appleboy)
### Documentation updates
* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
* docs: fix typo in comment (#3868) (@testwill)
* docs: fix typo in function documentation (#3872) (@TotomiEcio)
* docs: remove redundant comments (#3765) (@WeiTheShinobi)
* feat: update version constant to v1.10.0 (#3952) (@appleboy)
### Others
* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
* test(git): gitignore add develop tools (#3370) (@demoManito)
* test(http): use constant instead of numeric literal (#3863) (@testwill)
* test(path): Optimize unit test execution results (#3883) (@flc1125)
* test(render): increased unit tests coverage (#3691) (@araujo88)
## Gin v1.9.1
### BUG FIXES
* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
### SECURITY
* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
### ENHANCEMENTS
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
### DOCS
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
## Gin v1.9.0
### BREAK CHANGES
* 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
### 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
### BUG FIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0
### BREAKING
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### 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)
* chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
* path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
* Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
* Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
* tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
* tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
* tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
* use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
* 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)
* Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
* Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
* Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
* upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
* Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
* Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
* 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)
* fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
## Gin v1.5.0
- [FIX] Use DefaultWriter and DefaultErrorWriter for debug messages [#1891](https://github.com/gin-gonic/gin/pull/1891)
- [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
- [FIX] Some code improvements [#1909](https://github.com/gin-gonic/gin/pull/1909)
- [FIX] Use encode replace json marshal increase json encoder speed [#1546](https://github.com/gin-gonic/gin/pull/1546)
- [NEW] Hold matched route full path in the Context [#1826](https://github.com/gin-gonic/gin/pull/1826)
- [FIX] Fix context.Params race condition on Copy() [#1841](https://github.com/gin-gonic/gin/pull/1841)
- [NEW] Add context param query cache [#1450](https://github.com/gin-gonic/gin/pull/1450)
- [FIX] Improve GetQueryMap performance [#1918](https://github.com/gin-gonic/gin/pull/1918)
- [FIX] Improve get post data [#1920](https://github.com/gin-gonic/gin/pull/1920)
- [FIX] Use context instead of x/net/context [#1922](https://github.com/gin-gonic/gin/pull/1922)
- [FIX] Attempt to fix PostForm cache bug [#1931](https://github.com/gin-gonic/gin/pull/1931)
- [NEW] Add support of multipart multi files [#1949](https://github.com/gin-gonic/gin/pull/1949)
- [NEW] Support bind http header param [#1957](https://github.com/gin-gonic/gin/pull/1957)
- [FIX] Drop support for go1.8 and go1.9 [#1933](https://github.com/gin-gonic/gin/pull/1933)
- [FIX] Bugfix for the FullPath feature [#1919](https://github.com/gin-gonic/gin/pull/1919)
- [FIX] Gin1.5 bytes.Buffer to strings.Builder [#1939](https://github.com/gin-gonic/gin/pull/1939)
- [FIX] Upgrade github.com/ugorji/go/codec [#1969](https://github.com/gin-gonic/gin/pull/1969)
- [NEW] Support bind unix time [#1980](https://github.com/gin-gonic/gin/pull/1980)
- [FIX] Simplify code [#2004](https://github.com/gin-gonic/gin/pull/2004)
- [NEW] Support negative Content-Length in DataFromReader [#1981](https://github.com/gin-gonic/gin/pull/1981)
- [FIX] Identify terminal on a RISC-V architecture for auto-colored logs [#2019](https://github.com/gin-gonic/gin/pull/2019)
- [BREAKING] `Context.JSONP()` now expects a semicolon (`;`) at the end [#2007](https://github.com/gin-gonic/gin/pull/2007)
- [BREAKING] Upgrade default `binding.Validator` to v9 (see [its changelog](https://github.com/go-playground/validator/releases/tag/v9.0.0)) [#1015](https://github.com/gin-gonic/gin/pull/1015)
- [NEW] Add `DisallowUnknownFields()` in `Context.BindJSON()` [#2028](https://github.com/gin-gonic/gin/pull/2028)
- [NEW] Use specific `net.Listener` with `Engine.RunListener()` [#2023](https://github.com/gin-gonic/gin/pull/2023)
- [FIX] Fix some typo [#2079](https://github.com/gin-gonic/gin/pull/2079) [#2080](https://github.com/gin-gonic/gin/pull/2080)
- [FIX] Relocate binding body tests [#2086](https://github.com/gin-gonic/gin/pull/2086)
- [FIX] Use Writer in Context.Status [#1606](https://github.com/gin-gonic/gin/pull/1606)
- [FIX] `Engine.RunUnix()` now returns the error if it can't change the file mode [#2093](https://github.com/gin-gonic/gin/pull/2093)
- [FIX] `RouterGroup.StaticFS()` leaked files. Now it closes them. [#2118](https://github.com/gin-gonic/gin/pull/2118)
- [FIX] `Context.Request.FormFile` leaked file. Now it closes it. [#2114](https://github.com/gin-gonic/gin/pull/2114)
- [FIX] Ignore walking on `form:"-"` mapping [#1943](https://github.com/gin-gonic/gin/pull/1943)
### Gin v1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) - [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829) - [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) - [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) - [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) - [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
@ -15,7 +405,7 @@
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749) - [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252) - [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775) - [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260) - [NEW] Extend context.File to allow for the content-disposition attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112) - [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238) - [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020) - [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
@ -56,7 +446,7 @@
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491) - [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
### Gin 1.3.0 ## Gin v1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383) - [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358) - [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
@ -78,7 +468,7 @@
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250) - [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460) - [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
### Gin 1.2.0 ## Gin 1.2.0
- [NEW] Switch from godeps to govendor - [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls - [NEW] Add support for Let's Encrypt via gin-gonic/autotls
@ -101,15 +491,15 @@
- [FIX] Use X-Forwarded-For before X-Real-Ip - [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] time.Time binding (#904) - [FIX] time.Time binding (#904)
### Gin 1.1.4 ## Gin 1.1.4
- [NEW] Support google appengine for IsTerminal func - [NEW] Support google appengine for IsTerminal func
### Gin 1.1.3 ## Gin 1.1.3
- [FIX] Reverted Logger: skip ANSI color commands - [FIX] Reverted Logger: skip ANSI color commands
### Gin 1.1 ## Gin 1.1
- [NEW] Implement QueryArray and PostArray methods - [NEW] Implement QueryArray and PostArray methods
- [NEW] Refactor GetQuery and GetPostForm - [NEW] Refactor GetQuery and GetPostForm
@ -119,7 +509,7 @@
- [FIX] Changed imports to gopkg instead of github in README (#733) - [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty - [FIX] Logger: skip ANSI color commands if output is not a tty
### Gin 1.0rc2 (...) ## Gin 1.0rc2 (...)
- [PERFORMANCE] Fast path for writing Content-Type. - [PERFORMANCE] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing - [PERFORMANCE] Much faster 404 routing
@ -154,7 +544,7 @@
- [FIX] MIT license in every file - [FIX] MIT license in every file
### Gin 1.0rc1 (May 22, 2015) ## Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router - [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering - [PERFORMANCE] Faster JSON, XML and text rendering
@ -162,7 +552,7 @@
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations - [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
- [NEW] Built-in support for golang.org/x/net/context - [NEW] Built-in support for golang.org/x/net/context
- [NEW] Any(path, handler). Create a route that matches any path - [NEW] Any(path, handler). Create a route that matches any path
- [NEW] Refactored rendering pipeline (faster and static typeded) - [NEW] Refactored rendering pipeline (faster and static typed)
- [NEW] Refactored errors API - [NEW] Refactored errors API
- [NEW] IndentedJSON() prints pretty JSON - [NEW] IndentedJSON() prints pretty JSON
- [NEW] Added gin.DefaultWriter - [NEW] Added gin.DefaultWriter
@ -198,7 +588,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt) - [FIX] Better support for Google App Engine (using log instead of fmt)
### Gin 0.6 (Mar 9, 2015) ## Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data - [NEW] Support multipart/form-data
- [NEW] NoMethod handler - [NEW] NoMethod handler
@ -208,14 +598,14 @@
- [FIX] Improve color logger - [FIX] Improve color logger
### Gin 0.5 (Feb 7, 2015) ## Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation - [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip - [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding - [FIX] Fix unexported/ignored fields in binding
### Gin 0.4 (Aug 21, 2014) ## Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode - [NEW] Development mode
- [NEW] Unit tests - [NEW] Unit tests
@ -224,14 +614,14 @@
- [FIX] Improved documentation for model binding - [FIX] Improved documentation for model binding
### Gin 0.3 (Jul 18, 2014) ## Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call. - [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter() - [PERFORMANCE] Improve performance of NoRouter()
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults. - [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
- [NEW] Flexible rendering API - [NEW] Flexible rendering API
- [NEW] Add Context.File() - [NEW] Add Context.File()
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS - [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
- [FIX] Rename NotFound404() to NoRoute() - [FIX] Rename NotFound404() to NoRoute()
- [FIX] Errors in context are purged - [FIX] Errors in context are purged
- [FIX] Adds HEAD method in Static file serving - [FIX] Adds HEAD method in Static file serving
@ -242,7 +632,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form - [FIX] Check application/x-www-form-urlencoded when parsing form
### Gin 0.2b (Jul 08, 2014) ## Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration - [NEW] Travis CI integration
- [NEW] Completely new logger - [NEW] Completely new logger
@ -254,14 +644,14 @@
- [NEW] New Bind() and BindWith() methods for parsing request body. - [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy() - [NEW] Add Content.Copy()
- [NEW] Add context.LastError() - [NEW] Add context.LastError()
- [NEW] Add shorcut for OPTIONS HTTP method - [NEW] Add shortcut for OPTIONS HTTP method
- [FIX] Tons of README fixes - [FIX] Tons of README fixes
- [FIX] Header is written before body - [FIX] Header is written before body
- [FIX] BasicAuth() and changes API a little bit - [FIX] BasicAuth() and changes API a little bit
- [FIX] Recovery() middleware only prints panics - [FIX] Recovery() middleware only prints panics
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead. - [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
- [FIX] Multiple http.WriteHeader() in NotFound handlers - [FIX] Multiple http.WriteHeader() in NotFound handlers
- [FIX] Engine.Run() panics if http server can't be setted up - [FIX] Engine.Run() panics if http server can't be set up
- [FIX] Crash when route path doesn't start with '/' - [FIX] Crash when route path doesn't start with '/'
- [FIX] Do not update header when status code is negative - [FIX] Do not update header when status code is negative
- [FIX] Setting response headers before calling WriteHeader in context.String() - [FIX] Setting response headers before calling WriteHeader in context.String()

View File

@ -8,6 +8,6 @@
- With pull requests: - With pull requests:
- Open your pull request against `master` - Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them. - 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 integrations 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. - 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. - If your pull request contains a new feature, please document it on the README.

View File

@ -1,20 +1,17 @@
GO ?= go GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/) GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/) PACKAGES ?= $(shell $(GO) list ./...)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples) TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
TESTTAGS ?= ""
all: install
install: deps
govendor sync
.PHONY: test .PHONY: test
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \ $(GO) test $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \ cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \ if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \ rm tmp.out; \
@ -45,14 +42,10 @@ fmt-check:
exit 1; \ exit 1; \
fi; fi;
.PHONY: vet
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/kardianos/govendor; \
fi
.PHONY: lint .PHONY: lint
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@ -76,5 +69,10 @@ misspell:
.PHONY: tools .PHONY: tools
tools: tools:
go install golang.org/x/lint/golint; \ @if [ $(GO_VERSION) -gt 15 ]; then \
go install github.com/client9/misspell/cmd/misspell; $(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

2134
README.md

File diff suppressed because it is too large Load Diff

40
auth.go
View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -9,11 +9,16 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// AuthUserKey is the cookie name for user credential in basic auth. // AuthUserKey is the cookie name for user credential in basic auth.
const AuthUserKey = "user" const AuthUserKey = "user"
// AuthProxyUserKey is the cookie name for proxy_user credential in basic auth for proxy.
const AuthProxyUserKey = "proxy_user"
// Accounts defines a key/value for user/pass list of authorized logins. // Accounts defines a key/value for user/pass list of authorized logins.
type Accounts map[string]string type Accounts map[string]string
@ -29,7 +34,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false return "", false
} }
for _, pair := range a { for _, pair := range a {
if pair.value == authValue { if subtle.ConstantTimeCompare(bytesconv.StringToBytes(pair.value), bytesconv.StringToBytes(authValue)) == 1 {
return pair.user, true return pair.user, true
} }
} }
@ -69,8 +74,9 @@ func BasicAuth(accounts Accounts) HandlerFunc {
} }
func processAccounts(accounts Accounts) authPairs { func processAccounts(accounts Accounts) authPairs {
assert1(len(accounts) > 0, "Empty list of authorized credentials") length := len(accounts)
pairs := make(authPairs, 0, len(accounts)) assert1(length > 0, "Empty list of authorized credentials")
pairs := make(authPairs, 0, length)
for user, password := range accounts { for user, password := range accounts {
assert1(user != "", "User can not be empty") assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password) value := authorizationHeader(user, password)
@ -84,13 +90,27 @@ func processAccounts(accounts Accounts) authPairs {
func authorizationHeader(user, password string) string { func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
} }
func secureCompare(given, actual string) bool { // BasicAuthForProxy returns a Basic HTTP Proxy-Authorization middleware.
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { // If the realm is empty, "Proxy Authorization Required" will be used by default.
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 func BasicAuthForProxy(accounts Accounts, realm string) HandlerFunc {
if realm == "" {
realm = "Proxy Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
pairs := processAccounts(accounts)
return func(c *Context) {
proxyUser, found := pairs.searchCredential(c.requestHeader("Proxy-Authorization"))
if !found {
// Credentials doesn't match, we return 407 and abort handlers chain.
c.Header("Proxy-Authenticate", realm)
c.AbortWithStatus(http.StatusProxyAuthRequired)
return
}
// The proxy_user credentials was found, set proxy_user's id to key AuthProxyUserKey in this context, the proxy_user's id can be read later using
// c.MustGet(gin.AuthProxyUserKey).
c.Set(AuthProxyUserKey, proxyUser)
} }
// Securely compare actual to itself to keep constant time, but always return false.
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
} }

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password")) assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
} }
func TestBasicAuthSecureCompare(t *testing.T) {
assert.True(t, secureCompare("1234567890", "1234567890"))
assert.False(t, secureCompare("123456789", "1234567890"))
assert.False(t, secureCompare("12345678900", "1234567890"))
assert.False(t, secureCompare("1234567891", "1234567890"))
}
func TestBasicAuthSucceed(t *testing.T) { func TestBasicAuthSucceed(t *testing.T) {
accounts := Accounts{"admin": "password"} accounts := Accounts{"admin": "password"}
router := New() router := New()
@ -144,3 +137,40 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
} }
func TestBasicAuthForProxySucceed(t *testing.T) {
accounts := Accounts{"admin": "password"}
router := New()
router.Use(BasicAuthForProxy(accounts, ""))
router.Any("/*proxyPath", func(c *Context) {
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String())
}
func TestBasicAuthForProxy407(t *testing.T) {
called := false
accounts := Accounts{"foo": "bar"}
router := New()
router.Use(BasicAuthForProxy(accounts, ""))
router.Any("/*proxyPath", func(c *Context) {
called = true
c.String(http.StatusOK, c.MustGet(AuthProxyUserKey).(string))
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
assert.False(t, called)
assert.Equal(t, http.StatusProxyAuthRequired, w.Code)
assert.Equal(t, "Basic realm=\"Proxy Authorization Required\"", w.Header().Get("Proxy-Authenticate"))
}

View File

@ -1,4 +1,4 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,7 +1,9 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
package binding package binding
import "net/http" import "net/http"
@ -19,6 +21,8 @@ const (
MIMEMSGPACK = "application/x-msgpack" MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack" MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml" MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
) )
// Binding describes the interface which needs to be implemented for binding the // Binding describes the interface which needs to be implemented for binding the
@ -26,64 +30,67 @@ const (
// the form POST. // the form POST.
type Binding interface { type Binding interface {
Name() string Name() string
Bind(*http.Request, interface{}) error Bind(*http.Request, any) error
} }
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind, // BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body. // but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface { type BindingBody interface {
Binding Binding
BindBody([]byte, interface{}) error BindBody([]byte, any) error
} }
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind, // BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params. // but it reads the Params.
type BindingUri interface { type BindingUri interface {
Name() string 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 // 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 // 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 // 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 { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // 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 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. // If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned. // Otherwise nil must be returned.
ValidateStruct(interface{}) error ValidateStruct(any) error
// Engine returns the underlying validator engine which powers the // Engine returns the underlying validator engine which powers the
// StructValidator implementation. // StructValidator implementation.
Engine() interface{} Engine() any
} }
// Validator is the default validator which implements the StructValidator // 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. // under the hood.
var Validator StructValidator = &defaultValidator{} var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data // These implement the Binding interface and can be used to bind the data
// present in the request to struct instances. // present in the request to struct instances.
var ( var (
JSON = jsonBinding{} JSON BindingBody = jsonBinding{}
XML = xmlBinding{} XML BindingBody = xmlBinding{}
Form = formBinding{} Form Binding = formBinding{}
Query = queryBinding{} Query Binding = queryBinding{}
FormPost = formPostBinding{} FormPost Binding = formPostBinding{}
FormMultipart = formMultipartBinding{} FormMultipart Binding = formMultipartBinding{}
ProtoBuf = protobufBinding{} ProtoBuf BindingBody = protobufBinding{}
MsgPack = msgpackBinding{} MsgPack BindingBody = msgpackBinding{}
YAML = yamlBinding{} YAML BindingBody = yamlBinding{}
Uri = uriBinding{} Uri BindingUri = uriBinding{}
Header Binding = headerBinding{}
TOML BindingBody = tomlBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
// and the content type. // and the content type.
func Default(method, contentType string) Binding { func Default(method, contentType string) Binding {
if method == "GET" { if method == http.MethodGet {
return Form return Form
} }
@ -96,8 +103,10 @@ func Default(method, contentType string) Binding {
return ProtoBuf return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2: case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack return MsgPack
case MIMEYAML: case MIMEYAML, MIMEYAML2:
return YAML return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart return FormMultipart
default: // case MIMEPOSTForm: default: // case MIMEPOSTForm:
@ -105,7 +114,7 @@ func Default(method, contentType string) Binding {
} }
} }
func validate(obj interface{}) error { func validate(obj any) error {
if Validator == nil { if Validator == nil {
return nil return nil
} }

View File

@ -1,72 +0,0 @@
package binding
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingBody(t *testing.T) {
for _, tt := range []struct {
name string
binding BindingBody
body string
want string
}{
{
name: "JSON binding",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML binding",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: "MsgPack binding",
binding: MsgPack,
body: msgPackBody(t),
},
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
form := FooStruct{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, tt.binding.BindBody(body, &form))
assert.Equal(t, FooStruct{"FOO"}, form)
}
}
func msgPackBody(t *testing.T) string {
test := FooStruct{"FOO"}
h := new(codec.MsgpackHandle)
buf := bytes.NewBuffer(nil)
assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
return buf.String()
}
func TestBindingBodyProto(t *testing.T) {
test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
form := protoexample.Test{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, ProtoBuf.BindBody(body, &form))
assert.Equal(t, test, form)
}

View File

@ -0,0 +1,57 @@
// 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 !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
}

View File

@ -0,0 +1,116 @@
// 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 nomsgpack
package binding
import "net/http"
// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
)
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
Name() string
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, any) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it reads the Params.
type BindingUri interface {
Name() string
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/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(any) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() any
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEYAML, MIMEYAML2:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
case MIMETOML:
return TOML
default: // case MIMEPOSTForm:
return Form
}
}
func validate(obj any) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -9,23 +9,32 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"reflect"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin/testdata/protoexample" "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec" "google.golang.org/protobuf/proto"
) )
type appkey struct {
Appkey string `json:"appkey" form:"appkey"`
}
type QueryTest struct {
Page int `json:"page" form:"page"`
Size int `json:"size" form:"size"`
appkey
}
type FooStruct 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 { type FooBarStruct struct {
@ -51,12 +60,23 @@ type FooDefaultBarStruct struct {
} }
type FooStructUseNumber struct { type FooStructUseNumber struct {
Foo interface{} `json:"foo" binding:"required"` Foo any `json:"foo" binding:"required"`
}
type FooStructDisallowUnknownFields struct {
Foo any `json:"foo" binding:"required"`
} }
type FooBarStructForTimeType struct { type FooBarStructForTimeType struct {
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
} }
type FooStructForTimeTypeNotFormat struct { type FooStructForTimeTypeNotFormat struct {
@ -72,7 +92,7 @@ type FooStructForTimeTypeFailLocation struct {
} }
type FooStructForMapType struct { type FooStructForMapType struct {
MapFoo map[string]interface{} `form:"map_foo"` MapFoo map[string]any `form:"map_foo"`
} }
type FooStructForIgnoreFormTag struct { type FooStructForIgnoreFormTag struct {
@ -85,7 +105,7 @@ type InvalidNameType struct {
type InvalidNameMapType struct { type InvalidNameMapType struct {
TestName struct { TestName struct {
MapFoo map[string]interface{} `form:"map_foo"` MapFoo map[string]any `form:"map_foo"`
} }
} }
@ -107,7 +127,7 @@ type FooStructForStructPointerType struct {
type FooStructForSliceMapType struct { type FooStructForSliceMapType struct {
// Unknown type: not support map // Unknown type: not support map
SliceMapFoo []map[string]interface{} `form:"slice_map_foo"` SliceMapFoo []map[string]any `form:"slice_map_foo"`
} }
type FooStructForBoolType struct { type FooStructForBoolType struct {
@ -120,7 +140,7 @@ type FooStructForStringPtrType struct {
} }
type FooStructForMapPtrType struct { type FooStructForMapPtrType struct {
PtrBar *map[string]interface{} `form:"ptr_bar"` PtrBar *map[string]any `form:"ptr_bar"`
} }
func TestBindingDefault(t *testing.T) { func TestBindingDefault(t *testing.T) {
@ -142,11 +162,13 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, YAML, Default("POST", MIMEYAML)) assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML)) assert.Equal(t, YAML, Default("PUT", MIMEYAML))
assert.Equal(t, YAML, Default("POST", MIMEYAML2))
assert.Equal(t, YAML, Default("PUT", MIMEYAML2))
assert.Equal(t, TOML, Default("POST", MIMETOML))
assert.Equal(t, TOML, Default("PUT", MIMETOML))
} }
func TestBindingJSONNilBody(t *testing.T) { func TestBindingJSONNilBody(t *testing.T) {
@ -163,6 +185,20 @@ func TestBindingJSON(t *testing.T) {
`{"foo": "bar"}`, `{"bar": "foo"}`) `{"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) { func TestBindingJSONUseNumber(t *testing.T) {
testBodyBindingUseNumber(t, testBodyBindingUseNumber(t,
JSON, "json", JSON, "json",
@ -177,6 +213,18 @@ func TestBindingJSONUseNumber2(t *testing.T) {
`{"foo": 123}`, `{"bar": "foo"}`) `{"foo": 123}`, `{"bar": "foo"}`)
} }
func TestBindingJSONDisallowUnknownFields(t *testing.T) {
testBodyBindingDisallowUnknownFields(t, JSON,
"/", "/",
`{"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) { func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST", testFormBinding(t, "POST",
"/", "/", "/", "/",
@ -189,6 +237,18 @@ func TestBindingForm2(t *testing.T) {
"", "") "", "")
} }
func TestBindingFormEmbeddedStruct(t *testing.T) {
testFormBindingEmbeddedStruct(t, "POST",
"/", "/",
"page=1&size=2&appkey=test-appkey", "bar2=foo")
}
func TestBindingFormEmbeddedStruct2(t *testing.T) {
testFormBindingEmbeddedStruct(t, "GET",
"/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
"", "")
}
func TestBindingFormDefaultValue(t *testing.T) { func TestBindingFormDefaultValue(t *testing.T) {
testFormBindingDefaultValue(t, "POST", testFormBindingDefaultValue(t, "POST",
"/", "/", "/", "/",
@ -204,7 +264,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
func TestBindingFormForTime(t *testing.T) { func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST", testFormBindingForTime(t, "POST",
"/", "/", "/", "/",
"time_foo=2017-11-15&time_bar=", "bar2=foo") "time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "POST", testFormBindingForTimeNotFormat(t, "POST",
"/", "/", "/", "/",
"time_foo=2017-11-15", "bar2=foo") "time_foo=2017-11-15", "bar2=foo")
@ -218,8 +281,11 @@ func TestBindingFormForTime(t *testing.T) {
func TestBindingFormForTime2(t *testing.T) { func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, "GET", testFormBindingForTime(t, "GET",
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", "/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
"", "") "", "")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "GET", testFormBindingForTimeNotFormat(t, "GET",
"/?time_foo=2017-11-15", "/?bar2=foo", "/?time_foo=2017-11-15", "/?bar2=foo",
"", "") "", "")
@ -295,6 +361,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer") "", "", "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) { func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST", testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/", "/?foo=bar&bar=foo", "/",
@ -325,6 +422,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "") "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) { func TestBindingXML(t *testing.T) {
testBodyBinding(t, testBodyBinding(t,
XML, "xml", XML, "xml",
@ -339,6 +458,20 @@ func TestBindingXMLFail(t *testing.T) {
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>") "<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) { func TestBindingYAML(t *testing.T) {
testBodyBinding(t, testBodyBinding(t,
YAML, "yaml", YAML, "yaml",
@ -346,6 +479,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`) `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) { func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t, testBodyBindingFail(t,
YAML, "yaml", YAML, "yaml",
@ -396,7 +536,8 @@ func createFormFilesMultipartRequest(t *testing.T) *http.Request {
defer f.Close() defer f.Close()
fw, err1 := mw.CreateFormFile("file", "form.go") fw, err1 := mw.CreateFormFile("file", "form.go")
assert.NoError(t, err1) assert.NoError(t, err1)
io.Copy(fw, f) _, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2) assert.NoError(t, err2)
@ -420,7 +561,8 @@ func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
defer f.Close() defer f.Close()
fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go") fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go")
assert.NoError(t, err1) assert.NoError(t, err1)
io.Copy(fw, f) _, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2) assert.NoError(t, err2)
@ -509,17 +651,18 @@ func TestBindingFormPostForMapFail(t *testing.T) {
func TestBindingFormFilesMultipart(t *testing.T) { func TestBindingFormFilesMultipart(t *testing.T) {
req := createFormFilesMultipartRequest(t) req := createFormFilesMultipartRequest(t)
var obj FooBarFileStruct var obj FooBarFileStruct
FormMultipart.Bind(req, &obj) err := FormMultipart.Bind(req, &obj)
assert.NoError(t, err)
// file from os // file from os
f, _ := os.Open("form.go") f, _ := os.Open("form.go")
defer f.Close() defer f.Close()
fileActual, _ := ioutil.ReadAll(f) fileActual, _ := io.ReadAll(f)
// file from multipart // file from multipart
mf, _ := obj.File.Open() mf, _ := obj.File.Open()
defer mf.Close() defer mf.Close()
fileExpect, _ := ioutil.ReadAll(mf) fileExpect, _ := io.ReadAll(mf)
assert.Equal(t, FormMultipart.Name(), "multipart/form-data") assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
assert.Equal(t, obj.Foo, "bar") assert.Equal(t, obj.Foo, "bar")
@ -585,26 +728,6 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:])) string(data), string(data[1:]))
} }
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func TestValidationFails(t *testing.T) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`) req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@ -623,9 +746,9 @@ func TestValidationDisabled(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestExistsSucceeds(t *testing.T) { func TestRequiredSucceeds(t *testing.T) {
type HogeStruct struct { type HogeStruct struct {
Hoge *int `json:"hoge" binding:"exists"` Hoge *int `json:"hoge" binding:"required"`
} }
var obj HogeStruct var obj HogeStruct
@ -634,9 +757,9 @@ func TestExistsSucceeds(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestExistsFails(t *testing.T) { func TestRequiredFails(t *testing.T) {
type HogeStruct struct { type HogeStruct struct {
Hoge *int `json:"foo" binding:"exists"` Hoge *int `json:"foo" binding:"required"`
} }
var obj HogeStruct var obj HogeStruct
@ -645,6 +768,31 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestHeaderBinding(t *testing.T) {
h := Header
assert.Equal(t, "header", h.Name())
type tHeader struct {
Limit int `header:"limit"`
}
var theader tHeader
req := requestWithBody("GET", "/", "")
req.Header.Add("limit", "1000")
assert.NoError(t, h.Bind(req, &theader))
assert.Equal(t, 1000, theader.Limit)
req = requestWithBody("GET", "/", "")
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
Fail map[string]any `header:"fail"`
}
err := h.Bind(req, &failStruct{})
assert.Error(t, err)
}
func TestUriBinding(t *testing.T) { func TestUriBinding(t *testing.T) {
b := Uri b := Uri
assert.Equal(t, "uri", b.Name()) assert.Equal(t, "uri", b.Name())
@ -659,11 +807,11 @@ func TestUriBinding(t *testing.T) {
assert.Equal(t, "thinkerou", tag.Name) assert.Equal(t, "thinkerou", tag.Name)
type NotSupportStruct struct { type NotSupportStruct struct {
Name map[string]interface{} `uri:"name"` Name map[string]any `uri:"name"`
} }
var not NotSupportStruct var not NotSupportStruct
assert.Error(t, b.BindUri(m, &not)) 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) { func TestUriInnerBinding(t *testing.T) {
@ -688,6 +836,22 @@ func TestUriInnerBinding(t *testing.T) {
assert.Equal(t, tag.S.Age, expectedAge) assert.Equal(t, tag.S.Age, expectedAge)
} }
func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := QueryTest{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
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) { func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form b := Form
assert.Equal(t, "form", b.Name()) assert.Equal(t, "form", b.Name())
@ -785,6 +949,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String()) assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix()) assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
assert.Equal(t, "UTC", obj.TimeBar.Location().String()) assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
obj = FooBarStructForTimeType{} obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody) req = requestWithBody(method, badPath, badBody)
@ -792,6 +958,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Error(t, err) assert.Error(t, err)
} }
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeNotUnixFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.Error(t, err)
obj = FooStructForTimeTypeNotUnixFormat{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form b := Form
assert.Equal(t, "form", b.Name()) assert.Equal(t, "form", b.Name())
@ -924,9 +1108,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, assert.Equal(t,
struct { struct {
Idx int "form:\"idx\"" Idx int "form:\"idx\""
}(struct { }{Idx: 123},
Idx int "form:\"idx\""
}{Idx: 123}),
obj.StructFoo) obj.StructFoo)
case "StructPointer": case "StructPointer":
obj := FooStructForStructPointerType{} obj := FooStructForStructPointerType{}
@ -935,9 +1117,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, assert.Equal(t,
struct { struct {
Name string "form:\"name\"" Name string "form:\"name\""
}(struct { }{Name: "thinkerou"},
Name string "form:\"name\""
}{Name: "thinkerou"}),
*obj.StructPointerFoo) *obj.StructPointerFoo)
case "Map": case "Map":
obj := FooStructForMapType{} obj := FooStructForMapType{}
@ -1028,6 +1208,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err) 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) { func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
@ -1065,6 +1285,25 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.Error(t, err) assert.Error(t, err)
} }
func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath, body, badBody string) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
obj := FooStructDisallowUnknownFields{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStructDisallowUnknownFields{}
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
assert.Contains(t, err.Error(), "what")
}
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
@ -1109,11 +1348,18 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
obj := protoexample.Test{} obj := protoexample.Test{}
req := requestWithBody("POST", path, body) req := requestWithBody("POST", path, body)
req.Body = ioutil.NopCloser(&hook{}) req.Body = io.NopCloser(&hook{})
req.Header.Add("Content-Type", MIMEPROTOBUF) req.Header.Add("Content-Type", MIMEPROTOBUF)
err := b.Bind(req, &obj) err := b.Bind(req, &obj)
assert.Error(t, err) 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{} obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody) req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF) req.Header.Add("Content-Type", MIMEPROTOBUF)
@ -1121,23 +1367,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Error(t, err) assert.Error(t, err)
} }
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func requestWithBody(method, path, body string) (req *http.Request) { func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return return

View File

@ -1,14 +1,16 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strings"
"sync" "sync"
"gopkg.in/go-playground/validator.v8" "github.com/go-playground/validator/v10"
) )
type defaultValidator struct { type defaultValidator struct {
@ -16,36 +18,83 @@ type defaultValidator struct {
validate *validator.Validate 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. // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error { func (v *defaultValidator) ValidateStruct(obj any) error {
if obj == nil {
return nil
}
value := reflect.ValueOf(obj) value := reflect.ValueOf(obj)
valueType := value.Kind() switch value.Kind() {
if valueType == reflect.Ptr { case reflect.Ptr:
valueType = value.Elem().Kind() if value.Elem().Kind() != reflect.Struct {
} return v.ValidateStruct(value.Elem().Interface())
if valueType == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
return err
} }
return v.validateStruct(obj)
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
} }
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 // Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations // Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info - // or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8 // https://pkg.go.dev/github.com/go-playground/validator/v10
func (v *defaultValidator) Engine() interface{} { func (v *defaultValidator) Engine() any {
v.lazyinit() v.lazyinit()
return v.validate return v.validate
} }
func (v *defaultValidator) lazyinit() { func (v *defaultValidator) lazyinit() {
v.once.Do(func() { v.once.Do(func() {
config := &validator.Config{TagName: "binding"} v.validate = validator.New()
v.validate = validator.New(config) v.validate.SetTagName("binding")
}) })
} }

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

@ -1,16 +1,15 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package binding package binding
import ( import (
"mime/multipart" "errors"
"net/http" "net/http"
"reflect"
) )
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 << 20
type formBinding struct{} type formBinding struct{}
type formPostBinding struct{} type formPostBinding struct{}
@ -20,14 +19,12 @@ func (formBinding) Name() string {
return "form" 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 { if err := req.ParseForm(); err != nil {
return err return err
} }
if err := req.ParseMultipartForm(defaultMemory); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {
if err != http.ErrNotMultipart { return err
return err
}
} }
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err
@ -39,7 +36,7 @@ func (formPostBinding) Name() string {
return "form-urlencoded" 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 { if err := req.ParseForm(); err != nil {
return err return err
} }
@ -53,7 +50,7 @@ func (formMultipartBinding) Name() string {
return "multipart/form-data" 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 { if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err return err
} }
@ -63,27 +60,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
return validate(obj) return validate(obj)
} }
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
multipartFileHeaderStructType = reflect.TypeOf(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) {
if value.Type() == multipartFileHeaderStructType {
_, file, err := (*http.Request)(r).FormFile(key)
if err != nil {
return false, err
}
if file != nil {
value.Set(reflect.ValueOf(*file))
return true, nil
}
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,33 +7,62 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "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") 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") 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{} 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) return mappingByPtr(ptr, formSource(form), tag)
} }
// setter tries to set value on a walking by fields of a struct // setter tries to set value on a walking by fields of a struct
type setter interface { 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 type formSource map[string][]string
@ -41,17 +70,21 @@ type formSource map[string][]string
var _ setter = formSource(nil) var _ setter = formSource(nil)
// TrySet tries to set a value by request's form source (like map[string][]string) // 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) 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) _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
return err return err
} }
func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
var vKind = value.Kind() if field.Tag.Get(tag) == "-" { // just ignoring this field
return false, nil
}
vKind := value.Kind()
if vKind == reflect.Ptr { if vKind == reflect.Ptr {
var isNew bool var isNew bool
@ -60,39 +93,42 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
isNew = true isNew = true
vPtr = reflect.New(value.Type().Elem()) 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 { if err != nil {
return false, err return false, err
} }
if isNew && isSetted { if isNew && isSet {
value.Set(vPtr) value.Set(vPtr)
} }
return isSetted, nil return isSet, nil
} }
ok, err := tryToSetValue(value, field, setter, tag) if vKind != reflect.Struct || !field.Anonymous {
if err != nil { ok, err := tryToSetValue(value, field, setter, tag)
return false, err if err != nil {
} return false, err
if ok { }
return true, nil if ok {
return true, nil
}
} }
if vKind == reflect.Struct { if vKind == reflect.Struct {
tValue := value.Type() tValue := value.Type()
var isSetted bool var isSet bool
for i := 0; i < value.NumField(); i++ { for i := 0; i < value.NumField(); i++ {
if !value.Field(i).CanSet() { sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue continue
} }
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag) ok, err := mapping(value.Field(i), sf, setter, tag)
if err != nil { if err != nil {
return false, err return false, err
} }
isSetted = isSetted || ok isSet = isSet || ok
} }
return isSetted, nil return isSet, nil
} }
return false, nil return false, nil
} }
@ -109,9 +145,6 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
tagValue = field.Tag.Get(tag) tagValue = field.Tag.Get(tag)
tagValue, opts := head(tagValue, ",") tagValue, opts := head(tagValue, ",")
if tagValue == "-" { // just ignoring this field
return false, nil
}
if tagValue == "" { // default value is FieldName if tagValue == "" { // default value is FieldName
tagValue = field.Name tagValue = field.Name
} }
@ -123,9 +156,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
for len(opts) > 0 { for len(opts) > 0 {
opt, opts = head(opts, ",") opt, opts = head(opts, ",")
k, v := head(opt, "=") if k, v := head(opt, "="); k == "default" {
switch k {
case "default":
setOpt.isDefaultExists = true setOpt.isDefaultExists = true
setOpt.defaultValue = v setOpt.defaultValue = v
} }
@ -134,7 +165,24 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
return setter.TrySet(value, field, tagValue, setOpt) 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) { // BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param.
UnmarshalParam(param string) error
}
// trySetCustom tries to set a custom type value
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
switch v := value.Addr().Interface().(type) {
case BindUnmarshaler:
return true, v.UnmarshalParam(val)
}
return false, nil
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue] vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists { if !ok && !opt.isDefaultExists {
return false, nil return false, nil
@ -163,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if len(vs) > 0 { if len(vs) > 0 {
val = vs[0] val = vs[0]
} }
if ok, err := trySetCustom(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field) return true, setWithProperType(val, value, field)
} }
} }
@ -180,7 +231,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case reflect.Int64: case reflect.Int64:
switch value.Interface().(type) { switch value.Interface().(type) {
case time.Duration: case time.Duration:
return setTimeDuration(val, value, field) return setTimeDuration(val, value)
} }
return setIntField(val, 64, value) return setIntField(val, 64, value)
case reflect.Uint: case reflect.Uint:
@ -205,10 +256,17 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
switch value.Interface().(type) { switch value.Interface().(type) {
case time.Time: case time.Time:
return setTimeField(val, field, value) return setTimeField(val, field, value)
case multipart.FileHeader:
return nil
} }
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr:
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
return setWithProperType(val, value.Elem(), field)
default: default:
return errUnknownType return errUnknownType
} }
@ -265,6 +323,23 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339 timeFormat = time.RFC3339
} }
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" { if val == "" {
value.Set(reflect.ValueOf(time.Time{})) value.Set(reflect.ValueOf(time.Time{}))
return nil return nil
@ -312,7 +387,7 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
return nil 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) d, err := time.ParseDuration(val)
if err != nil { if err != nil {
return err return err
@ -322,9 +397,32 @@ func setTimeDuration(val string, value reflect.Value, field reflect.StructField)
} }
func head(str, sep string) (head string, tail string) { func head(str, sep string) (head string, tail string) {
idx := strings.Index(str, sep) head, tail, _ = strings.Cut(str, sep)
if idx < 0 { return head, tail
return str, "" }
}
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

@ -1,4 +1,4 @@
// Copyright 2019 Gin Core Team. All rights reserved. // Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -32,7 +32,10 @@ type structFull struct {
func BenchmarkMapFormFull(b *testing.B) { func BenchmarkMapFormFull(b *testing.B) {
var s structFull var s structFull
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
mapForm(&s, form) err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
} }
b.StopTimer() b.StopTimer()
@ -52,7 +55,10 @@ type structName struct {
func BenchmarkMapFormName(b *testing.B) { func BenchmarkMapFormName(b *testing.B) {
var s structName var s structName
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
mapForm(&s, form) err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
} }
b.StopTimer() b.StopTimer()

View File

@ -5,7 +5,11 @@
package binding package binding
import ( import (
"fmt"
"mime/multipart"
"reflect" "reflect"
"strconv"
"strings"
"testing" "testing"
"time" "time"
@ -18,9 +22,9 @@ func TestMappingBaseTypes(t *testing.T) {
} }
for _, tt := range []struct { for _, tt := range []struct {
name string name string
value interface{} value any
form string form string
expect interface{} expect any
}{ }{
{"base type", struct{ F int }{}, "9", int(9)}, {"base type", struct{ F int }{}, "9", int(9)},
{"base type", struct{ F int8 }{}, "9", int8(9)}, {"base type", struct{ F int8 }{}, "9", int8(9)},
@ -43,6 +47,7 @@ func TestMappingBaseTypes(t *testing.T) {
{"zero value", struct{ F uint }{}, "", uint(0)}, {"zero value", struct{ F uint }{}, "", uint(0)},
{"zero value", struct{ F bool }{}, "", false}, {"zero value", struct{ F bool }{}, "", false},
{"zero value", struct{ F float32 }{}, "", float32(0)}, {"zero value", struct{ F float32 }{}, "", float32(0)},
{"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}},
} { } {
tp := reflect.TypeOf(tt.value) tp := reflect.TypeOf(tt.value)
testName := tt.name + ":" + tp.Field(0).Type.String() testName := tt.name + ":" + tp.Field(0).Type.String()
@ -114,7 +119,7 @@ func TestMappingPrivateField(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form") err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int(0), s.f) assert.Equal(t, 0, s.f)
} }
func TestMappingUnknownFieldType(t *testing.T) { func TestMappingUnknownFieldType(t *testing.T) {
@ -131,9 +136,9 @@ func TestMappingURI(t *testing.T) {
var s struct { var s struct {
F int `uri:"field"` 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.NoError(t, err)
assert.Equal(t, int(6), s.F) assert.Equal(t, 6, s.F)
} }
func TestMappingForm(t *testing.T) { func TestMappingForm(t *testing.T) {
@ -142,7 +147,16 @@ func TestMappingForm(t *testing.T) {
} }
err := mapForm(&s, map[string][]string{"field": {"6"}}) err := mapForm(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err) 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) { func TestMappingTime(t *testing.T) {
@ -190,7 +204,7 @@ func TestMappingTime(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestMapiingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
var s struct { var s struct {
D time.Duration D time.Duration
} }
@ -260,6 +274,39 @@ func TestMappingStructField(t *testing.T) {
assert.Equal(t, 9, s.J.I) assert.Equal(t, 9, s.J.I)
} }
func TestMappingPtrField(t *testing.T) {
type ptrStruct struct {
Key int64 `json:"key"`
}
type ptrRequest struct {
Items []*ptrStruct `json:"items" form:"items"`
}
var err error
// With 0 items.
var req0 ptrRequest
err = mappingByPtr(&req0, formSource{}, "form")
assert.NoError(t, err)
assert.Empty(t, req0.Items)
// With 1 item.
var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
assert.NoError(t, err)
assert.Len(t, req1.Items, 1)
assert.EqualValues(t, 1, req1.Items[0].Key)
// With 2 items.
var req2 ptrRequest
err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form")
assert.NoError(t, err)
assert.Len(t, req2.Items, 2)
assert.EqualValues(t, 1, req2.Items[0].Key)
assert.EqualValues(t, 2, req2.Items[1].Key)
}
func TestMappingMapField(t *testing.T) { func TestMappingMapField(t *testing.T) {
var s struct { var s struct {
M map[string]int M map[string]int
@ -269,3 +316,109 @@ func TestMappingMapField(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M) assert.Equal(t, map[string]int{"one": 1}, s.M)
} }
func TestMappingIgnoredCircularRef(t *testing.T) {
type S struct {
S *S `form:"-"`
}
var s S
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
}
type customUnmarshalParamHex int
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
v, err := strconv.ParseInt(param, 16, 64)
if err != nil {
return err
}
*f = customUnmarshalParamHex(v)
return nil
}
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `form:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
assert.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `uri:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
assert.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
type customUnmarshalParamType struct {
Protocol string
Path string
Name string
}
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
parts := strings.Split(param, ":")
if len(parts) != 3 {
return fmt.Errorf("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
f.Name = parts[2]
return nil
}
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}

38
binding/header.go Normal file
View File

@ -0,0 +1,38 @@
// 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 (
"net/http"
"net/textproto"
"reflect"
)
type headerBinding struct{}
func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj any) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}
return validate(obj)
}
func mapHeader(ptr any, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}
type headerSource map[string][]string
var _ setter = headerSource(nil)
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

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -6,7 +6,7 @@ package binding
import ( import (
"bytes" "bytes"
"fmt" "errors"
"io" "io"
"net/http" "net/http"
@ -15,31 +15,40 @@ import (
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON // EnableDecoderUseNumber is used to call the UseNumber method on the JSON
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an // 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 var EnableDecoderUseNumber = false
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
// return an error when the destination is a struct and the input contains object
// keys which do not match any non-ignored, exported fields in the destination.
var EnableDecoderDisallowUnknownFields = false
type jsonBinding struct{} type jsonBinding struct{}
func (jsonBinding) Name() string { func (jsonBinding) Name() string {
return "json" 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 { if req == nil || req.Body == nil {
return fmt.Errorf("invalid request") return errors.New("invalid request")
} }
return decodeJSON(req.Body, obj) 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) 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) decoder := json.NewDecoder(r)
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }
if EnableDecoderDisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err
} }

30
binding/json_test.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2019 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 TestJSONBindingBindBody(t *testing.T) {
var s struct {
Foo string `json:"foo"`
}
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO"}`), &s)
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

@ -1,7 +1,9 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
package binding package binding
import ( import (
@ -18,15 +20,15 @@ func (msgpackBinding) Name() string {
return "msgpack" 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) 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) 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) cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil { if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err return err

34
binding/msgpack_test.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2019 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 !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestMsgpackBindingBindBody(t *testing.T) {
type teststruct struct {
Foo string `msgpack:"foo"`
}
var s teststruct
err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj any) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)
require.NoError(t, err)
return bs.Bytes()
}

View File

@ -0,0 +1,74 @@
// Copyright 2019 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"
"mime/multipart"
"net/http"
"reflect"
)
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) (bool, error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
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) {
case *multipart.FileHeader:
value.Set(reflect.ValueOf(files[0]))
return true, nil
}
case reflect.Struct:
switch value.Interface().(type) {
case multipart.FileHeader:
value.Set(reflect.ValueOf(*files[0]))
return true, nil
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
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, ErrMultiFileHeader
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) {
if value.Len() != len(files) {
return false, ErrMultiFileHeaderLenInvalid
}
for i := range files {
set, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !set {
return set, err
}
}
return true, nil
}

View File

@ -0,0 +1,138 @@
// Copyright 2019 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"
"mime/multipart"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormMultipartBindingBindOneFile(t *testing.T) {
var s struct {
FileValue multipart.FileHeader `form:"file"`
FilePtr *multipart.FileHeader `form:"file"`
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [1]multipart.FileHeader `form:"file"`
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
}
file := testFile{"file", "file1", []byte("hello")}
req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file)
assert.Len(t, s.SliceValues, 1)
assertMultipartFileHeader(t, &s.SliceValues[0], file)
assert.Len(t, s.SlicePtrs, 1)
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
}
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
var s struct {
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [2]multipart.FileHeader `form:"file"`
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
}
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files))
assert.Len(t, s.ArrayValues, len(files))
assert.Len(t, s.ArrayPtrs, len(files))
for i, file := range files {
assertMultipartFileHeader(t, &s.SliceValues[i], file)
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
}
}
func TestFormMultipartBindingBindError(t *testing.T) {
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
for _, tt := range []struct {
name string
s any
}{
{"wrong type", &struct {
Files int `form:"file"`
}{}},
{"wrong array size", &struct {
Files [1]*multipart.FileHeader `form:"file"`
}{}},
{"wrong slice type", &struct {
Files []int `form:"file"`
}{}},
} {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err)
}
}
type testFile struct {
Fieldname string
Filename string
Content []byte
}
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err)
n, err := fw.Write(file.Content)
assert.NoError(t, err)
assert.Equal(t, len(file.Content), n)
}
err := mw.Close()
assert.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req
}
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)
fl, err := fh.Open()
assert.NoError(t, err)
body, err := io.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
}

View File

@ -1,14 +1,15 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package binding package binding
import ( import (
"io/ioutil" "errors"
"io"
"net/http" "net/http"
"github.com/golang/protobuf/proto" "google.golang.org/protobuf/proto"
) )
type protobufBinding struct{} type protobufBinding struct{}
@ -17,16 +18,20 @@ func (protobufBinding) Name() string {
return "protobuf" return "protobuf"
} }
func (b protobufBinding) Bind(req *http.Request, obj interface{}) error { func (b protobufBinding) Bind(req *http.Request, obj any) error {
buf, err := ioutil.ReadAll(req.Body) buf, err := io.ReadAll(req.Body)
if err != nil { if err != nil {
return err return err
} }
return b.BindBody(buf, obj) return b.BindBody(buf, obj)
} }
func (protobufBinding) BindBody(body []byte, obj interface{}) error { func (protobufBinding) BindBody(body []byte, obj any) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil { msg, ok := obj.(proto.Message)
if !ok {
return errors.New("obj is not ProtoMessage")
}
if err := proto.Unmarshal(body, msg); err != nil {
return err return err
} }
// Here it's same to return validate(obj), but util now we can't add // Here it's same to return validate(obj), but util now we can't add

View File

@ -1,4 +1,4 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -12,7 +12,7 @@ func (queryBinding) Name() string {
return "query" return "query"
} }
func (queryBinding) Bind(req *http.Request, obj interface{}) error { func (queryBinding) Bind(req *http.Request, obj any) error {
values := req.URL.Query() values := req.URL.Query()
if err := mapForm(obj, values); err != nil { if err := mapForm(obj, values); err != nil {
return err 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

@ -1,4 +1,4 @@
// Copyright 2018 Gin Core Team. All rights reserved. // Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -10,8 +10,8 @@ func (uriBinding) Name() string {
return "uri" return "uri"
} }
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error { func (uriBinding) BindUri(m map[string][]string, obj any) error {
if err := mapUri(obj, m); err != nil { if err := mapURI(obj, m); err != nil {
return err return err
} }
return validate(obj) return validate(obj)

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -6,12 +6,11 @@ package binding
import ( import (
"bytes" "bytes"
"reflect"
"testing" "testing"
"time" "time"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
) )
type testInterface interface { type testInterface interface {
@ -60,7 +59,7 @@ type structNoValidationValues struct {
StructSlice []substructNoValidation StructSlice []substructNoValidation
InterfaceSlice []testInterface InterfaceSlice []testInterface
UniversalInterface interface{} UniversalInterface any
CustomInterface testInterface CustomInterface testInterface
FloatMap map[string]float32 FloatMap map[string]float32
@ -170,7 +169,7 @@ func TestValidateNoValidationPointers(t *testing.T) {
//assert.Equal(t, origin, test) //assert.Equal(t, origin, test)
} }
type Object map[string]interface{} type Object map[string]any
func TestValidatePrimitives(t *testing.T) { func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1} obj := Object{"foo": "bar", "bar": 1}
@ -193,6 +192,30 @@ func TestValidatePrimitives(t *testing.T) {
assert.Equal(t, "value", str) assert.Equal(t, "value", str)
} }
type structModifyValidation struct {
Integer int
}
func toZero(sl validator.StructLevel) {
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
s.Integer = 0
}
func TestValidateAndModifyStruct(t *testing.T) {
// This validates that pointers to structs are passed to the validator
// giving us the ability to modify the struct being validated.
engine, ok := Validator.Engine().(*validator.Validate)
assert.True(t, ok)
engine.RegisterStructValidation(toZero, structModifyValidation{})
s := structModifyValidation{Integer: 1}
errs := validate(&s)
assert.Nil(t, errs)
assert.Equal(t, s, structModifyValidation{Integer: 0})
}
// structCustomValidation is a helper struct we use to check that // structCustomValidation is a helper struct we use to check that
// custom validation can be registered on it. // custom validation can be registered on it.
// The `notone` binding directive is for custom validation and registered later. // The `notone` binding directive is for custom validation and registered later.
@ -200,15 +223,8 @@ type structCustomValidation struct {
Integer int `binding:"notone"` Integer int `binding:"notone"`
} }
// notOne is a custom validator meant to be used with `validator.v8` library. func notOne(f1 validator.FieldLevel) bool {
// The method signature for `v9` is significantly different and this function if val, ok := f1.Field().Interface().(int); ok {
// would need to be changed for tests to pass after upgrade.
// See https://github.com/gin-gonic/gin/pull/1015.
func notOne(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if val, ok := field.Interface().(int); ok {
return val != 1 return val != 1
} }
return false return false

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -17,14 +17,14 @@ func (xmlBinding) Name() string {
return "xml" 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) 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) 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) decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err

25
binding/xml_test.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2019 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 TestXMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `xml:"foo"`
}
xmlBody := `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`
err := xmlBinding{}.BindBody([]byte(xmlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -1,4 +1,4 @@
// Copyright 2018 Gin Core Team. All rights reserved. // Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -9,7 +9,7 @@ import (
"io" "io"
"net/http" "net/http"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
type yamlBinding struct{} type yamlBinding struct{}
@ -18,15 +18,15 @@ func (yamlBinding) Name() string {
return "yaml" 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) 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) 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) decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err

21
binding/yaml_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 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 TestYAMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `yaml:"foo"`
}
err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,19 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"os"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
) )
const ginSupportMinGoVer = 8 const ginSupportMinGoVer = 18
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -26,6 +24,9 @@ func IsDebugging() bool {
// DebugPrintRouteFunc indicates debug log output format. // DebugPrintRouteFunc indicates debug log output format.
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int) var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
// DebugPrintFunc indicates debug log output format.
var DebugPrintFunc func(format string, values ...interface{})
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
@ -40,7 +41,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
func debugPrintLoadTemplate(tmpl *template.Template) { func debugPrintLoadTemplate(tmpl *template.Template) {
if IsDebugging() { if IsDebugging() {
var buf bytes.Buffer var buf strings.Builder
for _, tmpl := range tmpl.Templates() { for _, tmpl := range tmpl.Templates() {
buf.WriteString("\t- ") buf.WriteString("\t- ")
buf.WriteString(tmpl.Name()) buf.WriteString(tmpl.Name())
@ -50,13 +51,20 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
} }
} }
func debugPrint(format string, values ...interface{}) { func debugPrint(format string, values ...any) {
if IsDebugging() { if !IsDebugging() {
if !strings.HasSuffix(format, "\n") { return
format += "\n"
}
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
} }
if DebugPrintFunc != nil {
DebugPrintFunc(format, values...)
return
}
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
} }
func getMinVer(v string) (uint64, error) { func getMinVer(v string) (uint64, error) {
@ -69,8 +77,8 @@ func getMinVer(v string) (uint64, error) {
} }
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.18+.
`) `)
} }
@ -98,7 +106,7 @@ at initialization. ie. before any route is registered or the router is listening
} }
func debugPrintError(err error) { func debugPrintError(err error) {
if err != nil { if err != nil && IsDebugging() {
debugPrint("[ERROR] %v\n", err) fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
} }
} }

View File

@ -1,17 +1,18 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"bytes"
"errors" "errors"
"fmt"
"html/template" "html/template"
"io" "io"
"log" "log"
"os" "os"
"runtime" "runtime"
"strings"
"sync" "sync"
"testing" "testing"
@ -20,7 +21,7 @@ import (
// TODO // TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { // func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...interface{}) { // func debugPrint(format string, values ...any) {
func TestIsDebugging(t *testing.T) { func TestIsDebugging(t *testing.T) {
SetMode(DebugMode) 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) 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) { func TestDebugPrintLoadTemplate(t *testing.T) {
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
@ -90,8 +103,8 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
SetMode(TestMode) SetMode(TestMode)
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer { if e == nil && m < ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.18+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { } else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }
@ -111,21 +124,21 @@ func captureOutput(t *testing.T, f func()) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }
stdout := os.Stdout defaultWriter := DefaultWriter
stderr := os.Stderr defaultErrorWriter := DefaultErrorWriter
defer func() { defer func() {
os.Stdout = stdout DefaultWriter = defaultWriter
os.Stderr = stderr DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
}() }()
os.Stdout = writer DefaultWriter = writer
os.Stderr = writer DefaultErrorWriter = writer
log.SetOutput(writer) log.SetOutput(writer)
out := make(chan string) out := make(chan string)
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)
wg.Add(1) wg.Add(1)
go func() { go func() {
var buf bytes.Buffer var buf strings.Builder
wg.Done() wg.Done()
_, err := io.Copy(&buf, reader) _, err := io.Copy(&buf, reader)
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -12,8 +12,10 @@ import (
// BindWith binds the passed struct pointer using the specified binding engine. // BindWith binds the passed struct pointer using the specified binding engine.
// See the binding package. // See the binding package.
func (c *Context) BindWith(obj interface{}, b binding.Binding) error { //
log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to // Deprecated: Use MustBindWith or ShouldBindWith.
func (c *Context) BindWith(obj any, b binding.Binding) error {
log.Println(`BindWith(\"any, binding.Binding\") error is going to
be deprecated, please check issue #662 and either use MustBindWith() if you 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 want HTTP 400 to be automatically returned if any error occur, or use
ShouldBindWith() if you need to manage the error.`) ShouldBindWith() if you need to manage the error.`)

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

2333
docs/doc.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
@ -34,12 +34,12 @@ const (
type Error struct { type Error struct {
Err error Err error
Type ErrorType Type ErrorType
Meta interface{} Meta any
} }
type errorMsgs []*Error type errorMsgs []*Error
var _ error = &Error{} var _ error = (*Error)(nil)
// SetType sets the error's type. // SetType sets the error's type.
func (msg *Error) SetType(flags ErrorType) *Error { 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. // SetMeta sets the error's meta data.
func (msg *Error) SetMeta(data interface{}) *Error { func (msg *Error) SetMeta(data any) *Error {
msg.Meta = data msg.Meta = data
return msg return msg
} }
// JSON creates a properly formatted JSON // JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} { func (msg *Error) JSON() any {
json := H{} jsonData := H{}
if msg.Meta != nil { if msg.Meta != nil {
value := reflect.ValueOf(msg.Meta) value := reflect.ValueOf(msg.Meta)
switch value.Kind() { switch value.Kind() {
@ -63,16 +63,16 @@ func (msg *Error) JSON() interface{} {
return msg.Meta return msg.Meta
case reflect.Map: case reflect.Map:
for _, key := range value.MapKeys() { for _, key := range value.MapKeys() {
json[key.String()] = value.MapIndex(key).Interface() jsonData[key.String()] = value.MapIndex(key).Interface()
} }
default: default:
json["meta"] = msg.Meta jsonData["meta"] = msg.Meta
} }
} }
if _, ok := json["error"]; !ok { if _, ok := jsonData["error"]; !ok {
json["error"] = msg.Error() jsonData["error"] = msg.Error()
} }
return json return jsonData
} }
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0 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. // ByType returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic. // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs { func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
@ -117,12 +122,13 @@ func (a errorMsgs) Last() *Error {
return nil return nil
} }
// Errors returns an array will all the error messages. // Errors returns an array with all the error messages.
// Example: // Example:
// c.Error(errors.New("first")) //
// c.Error(errors.New("second")) // c.Error(errors.New("first"))
// c.Error(errors.New("third")) // c.Error(errors.New("second"))
// c.Errors.Errors() // == []string{"first", "second", "third"} // c.Error(errors.New("third"))
// c.Errors.Errors() // == []string{"first", "second", "third"}
func (a errorMsgs) Errors() []string { func (a errorMsgs) Errors() []string {
if len(a) == 0 { if len(a) == 0 {
return nil return nil
@ -134,18 +140,18 @@ func (a errorMsgs) Errors() []string {
return errorStrings return errorStrings
} }
func (a errorMsgs) JSON() interface{} { func (a errorMsgs) JSON() any {
switch len(a) { switch length := len(a); length {
case 0: case 0:
return nil return nil
case 1: case 1:
return a.Last().JSON() return a.Last().JSON()
default: default:
json := make([]interface{}, len(a)) jsonData := make([]any, length)
for i, err := range a { for i, err := range a {
json[i] = err.JSON() jsonData[i] = err.JSON()
} }
return json return jsonData
} }
} }
@ -158,7 +164,7 @@ func (a errorMsgs) String() string {
if len(a) == 0 { if len(a) == 0 {
return "" return ""
} }
var buffer bytes.Buffer var buffer strings.Builder
for i, msg := range a { for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil { if msg.Meta != nil {

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -6,6 +6,7 @@ package gin
import ( import (
"errors" "errors"
"fmt"
"testing" "testing"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
@ -34,7 +35,7 @@ func TestError(t *testing.T) {
jsonBytes, _ := json.Marshal(err) jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ // nolint: errcheck err.SetMeta(H{ //nolint: errcheck
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
@ -44,7 +45,7 @@ func TestError(t *testing.T) {
"data": "some data", "data": "some data",
}, err.JSON()) }, err.JSON())
err.SetMeta(H{ // nolint: errcheck err.SetMeta(H{ //nolint: errcheck
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
@ -59,7 +60,7 @@ func TestError(t *testing.T) {
status string status string
data 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()) assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
} }
@ -85,7 +86,7 @@ Error #02: second
Error #03: third Error #03: third
Meta: map[status:400] Meta: map[status:400]
`, errs.String()) `, errs.String())
assert.Equal(t, []interface{}{ assert.Equal(t, []any{
H{"error": "first"}, H{"error": "first"},
H{"error": "second", "meta": "some data"}, H{"error": "second", "meta": "some data"},
H{"error": "third", "status": "400"}, H{"error": "third", "status": "400"},
@ -104,3 +105,24 @@ Error #03: third
assert.Nil(t, errs.JSON()) assert.Nil(t, errs.JSON())
assert.Empty(t, errs.String()) 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))
}

12
fs.go
View File

@ -1,4 +1,4 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -9,7 +9,7 @@ import (
"os" "os"
) )
type onlyfilesFS struct { type onlyFilesFS struct {
fs http.FileSystem fs http.FileSystem
} }
@ -17,7 +17,7 @@ type neuteredReaddirFile struct {
http.File 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(). // in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns // 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. // 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 { if listDirectory {
return fs return fs
} }
return &onlyfilesFS{fs} return &onlyFilesFS{fs}
} }
// Open conforms to http.Filesystem. // 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) f, err := fs.fs.Open(name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -39,7 +39,7 @@ func (fs onlyfilesFS) Open(name string) (http.File, error) {
} }
// Readdir overrides the http.File default implementation. // 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 // this disables directory listing
return nil, nil return nil, nil
} }

396
gin.go
View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -11,26 +11,49 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"regexp"
"strings"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
) )
const defaultMultipartMemory = 32 << 20 // 32 MB const defaultMultipartMemory = 32 << 20 // 32 MB
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
default405Body = []byte("405 method not allowed") 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. // HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array. // OptionFunc defines the function to change the default configuration
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own. // Last returns the last handler in the chain. i.e. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc { func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 { if length := len(c); length > 0 {
return c[length-1] return c[length-1]
@ -46,22 +69,34 @@ type RouteInfo struct {
HandlerFunc HandlerFunc HandlerFunc HandlerFunc
} }
// RoutesInfo defines a RouteInfo array. // RoutesInfo defines a RouteInfo slice.
type RoutesInfo []RouteInfo 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"
// PlatformFlyIO when running on Fly.io. Trust Fly-Client-IP for determining the client's IP
PlatformFlyIO = "Fly-Client-IP"
)
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default() // Create an instance of Engine, by using New() or Default()
type Engine struct { type Engine struct {
RouterGroup 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. // handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the // 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 // client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods. // and 307 for all other request methods.
RedirectTrailingSlash bool 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. // handle is registered for it.
// First superfluous path elements like ../ or // are removed. // First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path. // Afterwards the router does a case-insensitive lookup of the cleaned path.
@ -72,33 +107,60 @@ type Engine struct {
// RedirectTrailingSlash is independent of this option. // RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool 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. // current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed' // If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405. // and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound // If no other Method is allowed, the request is delegated to the NotFound
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with // 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
// 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. // 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool 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 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, // If UseRawPath is false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped. // as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool UnescapePathValues bool
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm // 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. // method call.
MaxMultipartMemory int64 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 delims render.Delims
secureJsonPrefix string secureJSONPrefix string
HTMLRender render.HTMLRender HTMLRender render.HTMLRender
FuncMap template.FuncMap FuncMap template.FuncMap
allNoRoute HandlersChain allNoRoute HandlersChain
@ -107,19 +169,23 @@ type Engine struct {
noMethod HandlersChain noMethod HandlersChain
pool sync.Pool pool sync.Pool
trees methodTrees 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. // New returns a new blank Engine instance without any middleware attached.
// By default the configuration is: // By default, the configuration is:
// - RedirectTrailingSlash: true // - RedirectTrailingSlash: true
// - RedirectFixedPath: false // - RedirectFixedPath: false
// - HandleMethodNotAllowed: false // - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true // - ForwardedByClientIP: true
// - UseRawPath: false // - UseRawPath: false
// - UnescapePathValues: true // - UnescapePathValues: true
func New() *Engine { func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
engine := &Engine{ engine := &Engine{
RouterGroup: RouterGroup{ RouterGroup: RouterGroup{
@ -132,42 +198,57 @@ func New() *Engine {
RedirectFixedPath: false, RedirectFixedPath: false,
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
AppEngine: defaultAppEngine, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false, UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true, UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory, MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"}, 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.RouterGroup.engine = engine
engine.pool.New = func() interface{} { engine.pool.New = func() any {
return engine.allocateContext() return engine.allocateContext(engine.maxParams)
} }
return engine return engine.With(opts...)
} }
// Default returns an Engine instance with the Logger and Recovery middleware already attached. // Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine { func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
engine := New() engine := New()
engine.Use(Logger(), Recovery()) engine.Use(Logger(), Recovery())
return engine return engine.With(opts...)
} }
func (engine *Engine) allocateContext() *Context { func (engine *Engine) Handler() http.Handler {
return &Context{engine: engine} 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 { func (engine *Engine) Delims(left, right string) *Engine {
engine.delims = render.Delims{Left: left, Right: right} engine.delims = render.Delims{Left: left, Right: right}
return engine 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 { func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix engine.secureJSONPrefix = prefix
return engine return engine
} }
@ -213,19 +294,19 @@ func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.FuncMap = 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) { func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.noRoute = handlers engine.noRoute = handlers
engine.rebuild404Handlers() engine.rebuild404Handlers()
} }
// NoMethod sets the handlers called when... TODO. // NoMethod sets the handlers called when Engine.HandleMethodNotAllowed = true.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) { func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.noMethod = handlers engine.noMethod = handlers
engine.rebuild405Handlers() 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... // 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. // For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
@ -235,6 +316,15 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine return engine
} }
// With returns a Engine with the configuration set in the OptionFunc.
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
}
return engine
}
func (engine *Engine) rebuild404Handlers() { func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute) engine.allNoRoute = engine.combineHandlers(engine.noRoute)
} }
@ -249,12 +339,22 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(len(handlers) > 0, "there must be at least one handler") assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers) debugPrintRoute(method, path, handlers)
root := engine.trees.get(method) root := engine.trees.get(method)
if root == nil { if root == nil {
root = new(node) root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root}) engine.trees = append(engine.trees, methodTree{method: method, root: root})
} }
root.addRoute(path, handlers) root.addRoute(path, handlers)
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: // Routes returns a slice of registered routes, including some useful information, such as:
@ -289,12 +389,120 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
func (engine *Engine) Run(addr ...string) (err error) { func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
address := resolveAddress(addr) address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address) debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine) err = http.ListenAndServe(address, engine.Handler())
return 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. // 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) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
@ -302,25 +510,35 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving HTTPS on %s\n", addr) debugPrint("Listening and serving HTTPS on %s\n", addr)
defer func() { debugPrintError(err) }() 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://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
return return
} }
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests // 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. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunUnix(file string) (err error) { func (engine *Engine) RunUnix(file string) (err error) {
debugPrint("Listening and serving HTTP on unix:/%s", file) debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
os.Remove(file) 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) listener, err := net.Listen("unix", file)
if err != nil { if err != nil {
return return
} }
defer listener.Close() defer listener.Close()
os.Chmod(file, 0777) defer os.Remove(file)
err = http.Serve(listener, engine)
err = http.Serve(listener, engine.Handler())
return return
} }
@ -331,13 +549,33 @@ func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd) debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }() 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)) f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f) listener, err := net.FileListener(f)
if err != nil { if err != nil {
return return
} }
defer listener.Close() defer listener.Close()
err = http.Serve(listener, engine) err = engine.RunListener(listener)
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.Serve(listener, engine.Handler())
return return
} }
@ -353,9 +591,9 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c) 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. // 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) { func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index oldIndexValue := c.index
c.reset() c.reset()
@ -373,6 +611,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} }
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method // Find root of the tree for the given HTTP method
t := engine.trees t := engine.trees
for i, tl := 0, len(t); i < tl; i++ { for i, tl := 0, len(t); i < tl; i++ {
@ -381,16 +623,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
handlers, params, tsr := root.getValue(rPath, c.Params, unescape) value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if handlers != nil { if value.params != nil {
c.handlers = handlers c.Params = *value.params
c.Params = params }
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next() c.Next()
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return return
} }
if httpMethod != "CONNECT" && rPath != "/" { if httpMethod != http.MethodConnect && rPath != "/" {
if tsr && engine.RedirectTrailingSlash { if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c) redirectTrailingSlash(c)
return return
} }
@ -402,17 +647,25 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
if engine.HandleMethodNotAllowed { if engine.HandleMethodNotAllowed {
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
// containing a list of the target resource's currently supported methods.
allowed := make([]string, 0, len(t)-1)
for _, tree := range engine.trees { for _, tree := range engine.trees {
if tree.method == httpMethod { if tree.method == httpMethod {
continue continue
} }
if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil { if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod allowed = append(allowed, tree.method)
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
} }
} }
if len(allowed) > 0 {
c.handlers = engine.allNoMethod
c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
} }
c.handlers = engine.allNoRoute c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body) serveError(c, http.StatusNotFound, default404Body)
} }
@ -434,27 +687,22 @@ func serveError(c *Context, code int, defaultMessage []byte) {
return return
} }
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return
} }
func redirectTrailingSlash(c *Context) { func redirectTrailingSlash(c *Context) {
req := c.Request req := c.Request
p := req.URL.Path p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { 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 p = prefix + "/" + req.URL.Path
} }
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = http.StatusTemporaryRedirect
}
req.URL.Path = p + "/" req.URL.Path = p + "/"
if length := len(p); length > 1 && p[length-1] == '/' { if length := len(p); length > 1 && p[length-1] == '/' {
req.URL.Path = p[:length-1] req.URL.Path = p[:length-1]
} }
debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String()) redirectRequest(c)
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
} }
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
@ -462,15 +710,23 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
rPath := req.URL.Path rPath := req.URL.Path
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok { if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method req.URL.Path = bytesconv.BytesToString(fixedPath)
if req.Method != "GET" { redirectRequest(c)
code = http.StatusTemporaryRedirect
}
req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
return true return true
} }
return false return false
} }
func redirectRequest(c *Context) {
req := c.Request
rPath := req.URL.Path
rURL := req.URL.String()
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != http.MethodGet {
code = http.StatusTemporaryRedirect
}
debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
http.Redirect(c.Writer, req, rURL, code)
c.writermem.WriteHeaderNow()
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -37,7 +37,7 @@ func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ) 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) { func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...) engine().NoRoute(handlers...)
} }
@ -108,7 +108,8 @@ func StaticFile(relativePath, filepath string) gin.IRoutes {
// of the Router's NotFound handler. // of the Router's NotFound handler.
// To use the operating system's file system implementation, // To use the operating system's file system implementation,
// use : // use :
// router.Static("/static", "/var/www") //
// router.Static("/static", "/var/www")
func Static(relativePath, root string) gin.IRoutes { func Static(relativePath, root string) gin.IRoutes {
return engine().Static(relativePath, root) return engine().Static(relativePath, root)
} }
@ -118,7 +119,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs) 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... // 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. // For example, this is the right place for a logger or error management middleware.
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { 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 // 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. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunUnix(file string) (err error) { func RunUnix(file string) (err error) {
return engine().RunUnix(file) return engine().RunUnix(file)

View File

@ -1,4 +1,4 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -9,11 +9,13 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath"
"runtime"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -21,7 +23,15 @@ import (
"github.com/stretchr/testify/assert" "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{ tr := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
@ -29,14 +39,27 @@ func testRequest(t *testing.T, url string) {
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get(url) resp, err := client.Get(params[0])
assert.NoError(t, err) assert.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body) body, ioerr := io.ReadAll(resp.Body)
assert.NoError(t, ioerr) 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) { func TestRunEmpty(t *testing.T) {
@ -54,6 +77,81 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") 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) { func TestRunTLS(t *testing.T) {
router := New() router := New()
go func() { go func() {
@ -90,7 +188,8 @@ func TestPusher(t *testing.T) {
go func() { go func() {
router.GET("/pusher", func(c *Context) { router.GET("/pusher", func(c *Context) {
if pusher := c.Writer.Pusher(); pusher != nil { if pusher := c.Writer.Pusher(); pusher != nil {
pusher.Push("/assets/app.js", nil) err := pusher.Push("/assets/app.js", nil)
assert.NoError(t, err)
} }
c.String(http.StatusOK, "it worked") c.String(http.StatusOK, "it worked")
}) })
@ -145,15 +244,19 @@ func TestRunWithPort(t *testing.T) {
func TestUnixSocket(t *testing.T) { func TestUnixSocket(t *testing.T) {
router := New() router := New()
unixTestSocket := filepath.Join(os.TempDir(), "unix_unit_test")
defer os.Remove(unixTestSocket)
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) 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 // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", "/tmp/unix_unit_test") c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err) assert.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
@ -179,7 +282,16 @@ func TestFileDescriptor(t *testing.T) {
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err) assert.NoError(t, err)
socketFile, err := listener.File() socketFile, err := listener.File()
assert.NoError(t, err) 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() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -207,6 +319,43 @@ func TestBadFileDescriptor(t *testing.T) {
assert.Error(t, router.RunFd(0)) assert.Error(t, router.RunFd(0))
} }
func TestListener(t *testing.T) {
router := New()
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.NoError(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)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
listener.Close()
assert.Error(t, router.RunListener(listener))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New() router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -253,7 +402,7 @@ func TestConcurrentHandleContext(t *testing.T) {
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) { func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err) assert.NoError(t, err)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -262,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, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200") 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

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -8,16 +8,19 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/http2"
) )
func formatAsDate(t time.Time) string { func formatAsDate(t time.Time) string {
@ -41,7 +44,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
}) })
router.GET("/raw", func(c *Context) { 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), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
@ -71,10 +74,48 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -91,10 +132,10 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -111,10 +152,10 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -138,10 +179,10 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -158,11 +199,11 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) t.Error(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp)) assert.Equal(t, "Date: 2017/07/01", string(resp))
} }
func init() { func init() {
@ -189,10 +230,10 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -209,10 +250,10 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -229,10 +270,10 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -256,10 +297,10 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -276,11 +317,11 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL)) res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) t.Error(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp)) assert.Equal(t, "Date: 2017/07/01", string(resp))
} }
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
@ -394,7 +435,6 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
} }
func TestRebuild404Handlers(t *testing.T) { func TestRebuild404Handlers(t *testing.T) {
} }
func TestNoMethodWithGlobalHandlers(t *testing.T) { func TestNoMethodWithGlobalHandlers(t *testing.T) {
@ -428,7 +468,7 @@ func TestNoMethodWithGlobalHandlers(t *testing.T) {
compareFunc(t, router.allNoMethod[2], middleware0) 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) sf1 := reflect.ValueOf(a)
sf2 := reflect.ValueOf(b) sf2 := reflect.ValueOf(b)
if sf1.Pointer() != sf2.Pointer() { if sf1.Pointer() != sf2.Pointer() {
@ -490,7 +530,7 @@ func TestEngineHandleContext(t *testing.T) {
} }
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/") w := PerformRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code) assert.Equal(t, 301, w.Code)
}) })
} }
@ -523,7 +563,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
}) })
assert.NotPanics(t, func() { 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, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len()) assert.Equal(t, expectValue, w.Body.Len())
}) })
@ -532,6 +572,119 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter) 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) { func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes { for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
@ -544,3 +697,60 @@ func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo)
func handlerTest1(c *Context) {} func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {} func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
}
r := New(fc)
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
}
func TestWithOptionFunc(t *testing.T) {
r := New()
r.With(func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
})
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
}
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1))
return nil
}
func TestCustomUnmarshalStruct(t *testing.T) {
route := Default()
var request struct {
Birthday Birthday `form:"birthday"`
}
route.GET("/test", func(ctx *Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
w := httptest.NewRecorder()
route.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, `"2000/01/01"`, w.Body.String())
}

View File

@ -1,16 +1,16 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -24,265 +24,265 @@ type route struct {
// http://developer.github.com/v3/ // http://developer.github.com/v3/
var githubAPI = []route{ var githubAPI = []route{
// OAuth Authorizations // OAuth Authorizations
{"GET", "/authorizations"}, {http.MethodGet, "/authorizations"},
{"GET", "/authorizations/:id"}, {http.MethodGet, "/authorizations/:id"},
{"POST", "/authorizations"}, {http.MethodPost, "/authorizations"},
//{"PUT", "/authorizations/clients/:client_id"}, //{http.MethodPut, "/authorizations/clients/:client_id"},
//{"PATCH", "/authorizations/:id"}, //{http.MethodPatch, "/authorizations/:id"},
{"DELETE", "/authorizations/:id"}, {http.MethodDelete, "/authorizations/:id"},
{"GET", "/applications/:client_id/tokens/:access_token"}, {http.MethodGet, "/applications/:client_id/tokens/:access_token"},
{"DELETE", "/applications/:client_id/tokens"}, {http.MethodDelete, "/applications/:client_id/tokens"},
{"DELETE", "/applications/:client_id/tokens/:access_token"}, {http.MethodDelete, "/applications/:client_id/tokens/:access_token"},
// Activity // Activity
{"GET", "/events"}, {http.MethodGet, "/events"},
{"GET", "/repos/:owner/:repo/events"}, {http.MethodGet, "/repos/:owner/:repo/events"},
{"GET", "/networks/:owner/:repo/events"}, {http.MethodGet, "/networks/:owner/:repo/events"},
{"GET", "/orgs/:org/events"}, {http.MethodGet, "/orgs/:org/events"},
{"GET", "/users/:user/received_events"}, {http.MethodGet, "/users/:user/received_events"},
{"GET", "/users/:user/received_events/public"}, {http.MethodGet, "/users/:user/received_events/public"},
{"GET", "/users/:user/events"}, {http.MethodGet, "/users/:user/events"},
{"GET", "/users/:user/events/public"}, {http.MethodGet, "/users/:user/events/public"},
{"GET", "/users/:user/events/orgs/:org"}, {http.MethodGet, "/users/:user/events/orgs/:org"},
{"GET", "/feeds"}, {http.MethodGet, "/feeds"},
{"GET", "/notifications"}, {http.MethodGet, "/notifications"},
{"GET", "/repos/:owner/:repo/notifications"}, {http.MethodGet, "/repos/:owner/:repo/notifications"},
{"PUT", "/notifications"}, {http.MethodPut, "/notifications"},
{"PUT", "/repos/:owner/:repo/notifications"}, {http.MethodPut, "/repos/:owner/:repo/notifications"},
{"GET", "/notifications/threads/:id"}, {http.MethodGet, "/notifications/threads/:id"},
//{"PATCH", "/notifications/threads/:id"}, //{http.MethodPatch, "/notifications/threads/:id"},
{"GET", "/notifications/threads/:id/subscription"}, {http.MethodGet, "/notifications/threads/:id/subscription"},
{"PUT", "/notifications/threads/:id/subscription"}, {http.MethodPut, "/notifications/threads/:id/subscription"},
{"DELETE", "/notifications/threads/:id/subscription"}, {http.MethodDelete, "/notifications/threads/:id/subscription"},
{"GET", "/repos/:owner/:repo/stargazers"}, {http.MethodGet, "/repos/:owner/:repo/stargazers"},
{"GET", "/users/:user/starred"}, {http.MethodGet, "/users/:user/starred"},
{"GET", "/user/starred"}, {http.MethodGet, "/user/starred"},
{"GET", "/user/starred/:owner/:repo"}, {http.MethodGet, "/user/starred/:owner/:repo"},
{"PUT", "/user/starred/:owner/:repo"}, {http.MethodPut, "/user/starred/:owner/:repo"},
{"DELETE", "/user/starred/:owner/:repo"}, {http.MethodDelete, "/user/starred/:owner/:repo"},
{"GET", "/repos/:owner/:repo/subscribers"}, {http.MethodGet, "/repos/:owner/:repo/subscribers"},
{"GET", "/users/:user/subscriptions"}, {http.MethodGet, "/users/:user/subscriptions"},
{"GET", "/user/subscriptions"}, {http.MethodGet, "/user/subscriptions"},
{"GET", "/repos/:owner/:repo/subscription"}, {http.MethodGet, "/repos/:owner/:repo/subscription"},
{"PUT", "/repos/:owner/:repo/subscription"}, {http.MethodPut, "/repos/:owner/:repo/subscription"},
{"DELETE", "/repos/:owner/:repo/subscription"}, {http.MethodDelete, "/repos/:owner/:repo/subscription"},
{"GET", "/user/subscriptions/:owner/:repo"}, {http.MethodGet, "/user/subscriptions/:owner/:repo"},
{"PUT", "/user/subscriptions/:owner/:repo"}, {http.MethodPut, "/user/subscriptions/:owner/:repo"},
{"DELETE", "/user/subscriptions/:owner/:repo"}, {http.MethodDelete, "/user/subscriptions/:owner/:repo"},
// Gists // Gists
{"GET", "/users/:user/gists"}, {http.MethodGet, "/users/:user/gists"},
{"GET", "/gists"}, {http.MethodGet, "/gists"},
//{"GET", "/gists/public"}, //{http.MethodGet, "/gists/public"},
//{"GET", "/gists/starred"}, //{http.MethodGet, "/gists/starred"},
{"GET", "/gists/:id"}, {http.MethodGet, "/gists/:id"},
{"POST", "/gists"}, {http.MethodPost, "/gists"},
//{"PATCH", "/gists/:id"}, //{http.MethodPatch, "/gists/:id"},
{"PUT", "/gists/:id/star"}, {http.MethodPut, "/gists/:id/star"},
{"DELETE", "/gists/:id/star"}, {http.MethodDelete, "/gists/:id/star"},
{"GET", "/gists/:id/star"}, {http.MethodGet, "/gists/:id/star"},
{"POST", "/gists/:id/forks"}, {http.MethodPost, "/gists/:id/forks"},
{"DELETE", "/gists/:id"}, {http.MethodDelete, "/gists/:id"},
// Git Data // Git Data
{"GET", "/repos/:owner/:repo/git/blobs/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/blobs/:sha"},
{"POST", "/repos/:owner/:repo/git/blobs"}, {http.MethodPost, "/repos/:owner/:repo/git/blobs"},
{"GET", "/repos/:owner/:repo/git/commits/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/commits/:sha"},
{"POST", "/repos/:owner/:repo/git/commits"}, {http.MethodPost, "/repos/:owner/:repo/git/commits"},
//{"GET", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodGet, "/repos/:owner/:repo/git/refs/*ref"},
{"GET", "/repos/:owner/:repo/git/refs"}, {http.MethodGet, "/repos/:owner/:repo/git/refs"},
{"POST", "/repos/:owner/:repo/git/refs"}, {http.MethodPost, "/repos/:owner/:repo/git/refs"},
//{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodPatch, "/repos/:owner/:repo/git/refs/*ref"},
//{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, //{http.MethodDelete, "/repos/:owner/:repo/git/refs/*ref"},
{"GET", "/repos/:owner/:repo/git/tags/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/tags/:sha"},
{"POST", "/repos/:owner/:repo/git/tags"}, {http.MethodPost, "/repos/:owner/:repo/git/tags"},
{"GET", "/repos/:owner/:repo/git/trees/:sha"}, {http.MethodGet, "/repos/:owner/:repo/git/trees/:sha"},
{"POST", "/repos/:owner/:repo/git/trees"}, {http.MethodPost, "/repos/:owner/:repo/git/trees"},
// Issues // Issues
{"GET", "/issues"}, {http.MethodGet, "/issues"},
{"GET", "/user/issues"}, {http.MethodGet, "/user/issues"},
{"GET", "/orgs/:org/issues"}, {http.MethodGet, "/orgs/:org/issues"},
{"GET", "/repos/:owner/:repo/issues"}, {http.MethodGet, "/repos/:owner/:repo/issues"},
{"GET", "/repos/:owner/:repo/issues/:number"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number"},
{"POST", "/repos/:owner/:repo/issues"}, {http.MethodPost, "/repos/:owner/:repo/issues"},
//{"PATCH", "/repos/:owner/:repo/issues/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/issues/:number"},
{"GET", "/repos/:owner/:repo/assignees"}, {http.MethodGet, "/repos/:owner/:repo/assignees"},
{"GET", "/repos/:owner/:repo/assignees/:assignee"}, {http.MethodGet, "/repos/:owner/:repo/assignees/:assignee"},
{"GET", "/repos/:owner/:repo/issues/:number/comments"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/comments"},
//{"GET", "/repos/:owner/:repo/issues/comments"}, //{http.MethodGet, "/repos/:owner/:repo/issues/comments"},
//{"GET", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodGet, "/repos/:owner/:repo/issues/comments/:id"},
{"POST", "/repos/:owner/:repo/issues/:number/comments"}, {http.MethodPost, "/repos/:owner/:repo/issues/:number/comments"},
//{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/issues/comments/:id"},
//{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, //{http.MethodDelete, "/repos/:owner/:repo/issues/comments/:id"},
{"GET", "/repos/:owner/:repo/issues/:number/events"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/events"},
//{"GET", "/repos/:owner/:repo/issues/events"}, //{http.MethodGet, "/repos/:owner/:repo/issues/events"},
//{"GET", "/repos/:owner/:repo/issues/events/:id"}, //{http.MethodGet, "/repos/:owner/:repo/issues/events/:id"},
{"GET", "/repos/:owner/:repo/labels"}, {http.MethodGet, "/repos/:owner/:repo/labels"},
{"GET", "/repos/:owner/:repo/labels/:name"}, {http.MethodGet, "/repos/:owner/:repo/labels/:name"},
{"POST", "/repos/:owner/:repo/labels"}, {http.MethodPost, "/repos/:owner/:repo/labels"},
//{"PATCH", "/repos/:owner/:repo/labels/:name"}, //{http.MethodPatch, "/repos/:owner/:repo/labels/:name"},
{"DELETE", "/repos/:owner/:repo/labels/:name"}, {http.MethodDelete, "/repos/:owner/:repo/labels/:name"},
{"GET", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodGet, "/repos/:owner/:repo/issues/:number/labels"},
{"POST", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodPost, "/repos/:owner/:repo/issues/:number/labels"},
{"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels/:name"},
{"PUT", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodPut, "/repos/:owner/:repo/issues/:number/labels"},
{"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, {http.MethodDelete, "/repos/:owner/:repo/issues/:number/labels"},
{"GET", "/repos/:owner/:repo/milestones/:number/labels"}, {http.MethodGet, "/repos/:owner/:repo/milestones/:number/labels"},
{"GET", "/repos/:owner/:repo/milestones"}, {http.MethodGet, "/repos/:owner/:repo/milestones"},
{"GET", "/repos/:owner/:repo/milestones/:number"}, {http.MethodGet, "/repos/:owner/:repo/milestones/:number"},
{"POST", "/repos/:owner/:repo/milestones"}, {http.MethodPost, "/repos/:owner/:repo/milestones"},
//{"PATCH", "/repos/:owner/:repo/milestones/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/milestones/:number"},
{"DELETE", "/repos/:owner/:repo/milestones/:number"}, {http.MethodDelete, "/repos/:owner/:repo/milestones/:number"},
// Miscellaneous // Miscellaneous
{"GET", "/emojis"}, {http.MethodGet, "/emojis"},
{"GET", "/gitignore/templates"}, {http.MethodGet, "/gitignore/templates"},
{"GET", "/gitignore/templates/:name"}, {http.MethodGet, "/gitignore/templates/:name"},
{"POST", "/markdown"}, {http.MethodPost, "/markdown"},
{"POST", "/markdown/raw"}, {http.MethodPost, "/markdown/raw"},
{"GET", "/meta"}, {http.MethodGet, "/meta"},
{"GET", "/rate_limit"}, {http.MethodGet, "/rate_limit"},
// Organizations // Organizations
{"GET", "/users/:user/orgs"}, {http.MethodGet, "/users/:user/orgs"},
{"GET", "/user/orgs"}, {http.MethodGet, "/user/orgs"},
{"GET", "/orgs/:org"}, {http.MethodGet, "/orgs/:org"},
//{"PATCH", "/orgs/:org"}, //{http.MethodPatch, "/orgs/:org"},
{"GET", "/orgs/:org/members"}, {http.MethodGet, "/orgs/:org/members"},
{"GET", "/orgs/:org/members/:user"}, {http.MethodGet, "/orgs/:org/members/:user"},
{"DELETE", "/orgs/:org/members/:user"}, {http.MethodDelete, "/orgs/:org/members/:user"},
{"GET", "/orgs/:org/public_members"}, {http.MethodGet, "/orgs/:org/public_members"},
{"GET", "/orgs/:org/public_members/:user"}, {http.MethodGet, "/orgs/:org/public_members/:user"},
{"PUT", "/orgs/:org/public_members/:user"}, {http.MethodPut, "/orgs/:org/public_members/:user"},
{"DELETE", "/orgs/:org/public_members/:user"}, {http.MethodDelete, "/orgs/:org/public_members/:user"},
{"GET", "/orgs/:org/teams"}, {http.MethodGet, "/orgs/:org/teams"},
{"GET", "/teams/:id"}, {http.MethodGet, "/teams/:id"},
{"POST", "/orgs/:org/teams"}, {http.MethodPost, "/orgs/:org/teams"},
//{"PATCH", "/teams/:id"}, //{http.MethodPatch, "/teams/:id"},
{"DELETE", "/teams/:id"}, {http.MethodDelete, "/teams/:id"},
{"GET", "/teams/:id/members"}, {http.MethodGet, "/teams/:id/members"},
{"GET", "/teams/:id/members/:user"}, {http.MethodGet, "/teams/:id/members/:user"},
{"PUT", "/teams/:id/members/:user"}, {http.MethodPut, "/teams/:id/members/:user"},
{"DELETE", "/teams/:id/members/:user"}, {http.MethodDelete, "/teams/:id/members/:user"},
{"GET", "/teams/:id/repos"}, {http.MethodGet, "/teams/:id/repos"},
{"GET", "/teams/:id/repos/:owner/:repo"}, {http.MethodGet, "/teams/:id/repos/:owner/:repo"},
{"PUT", "/teams/:id/repos/:owner/:repo"}, {http.MethodPut, "/teams/:id/repos/:owner/:repo"},
{"DELETE", "/teams/:id/repos/:owner/:repo"}, {http.MethodDelete, "/teams/:id/repos/:owner/:repo"},
{"GET", "/user/teams"}, {http.MethodGet, "/user/teams"},
// Pull Requests // Pull Requests
{"GET", "/repos/:owner/:repo/pulls"}, {http.MethodGet, "/repos/:owner/:repo/pulls"},
{"GET", "/repos/:owner/:repo/pulls/:number"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number"},
{"POST", "/repos/:owner/:repo/pulls"}, {http.MethodPost, "/repos/:owner/:repo/pulls"},
//{"PATCH", "/repos/:owner/:repo/pulls/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/pulls/:number"},
{"GET", "/repos/:owner/:repo/pulls/:number/commits"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/commits"},
{"GET", "/repos/:owner/:repo/pulls/:number/files"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/files"},
{"GET", "/repos/:owner/:repo/pulls/:number/merge"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/merge"},
{"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, {http.MethodPut, "/repos/:owner/:repo/pulls/:number/merge"},
{"GET", "/repos/:owner/:repo/pulls/:number/comments"}, {http.MethodGet, "/repos/:owner/:repo/pulls/:number/comments"},
//{"GET", "/repos/:owner/:repo/pulls/comments"}, //{http.MethodGet, "/repos/:owner/:repo/pulls/comments"},
//{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodGet, "/repos/:owner/:repo/pulls/comments/:number"},
{"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, {http.MethodPut, "/repos/:owner/:repo/pulls/:number/comments"},
//{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodPatch, "/repos/:owner/:repo/pulls/comments/:number"},
//{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, //{http.MethodDelete, "/repos/:owner/:repo/pulls/comments/:number"},
// Repositories // Repositories
{"GET", "/user/repos"}, {http.MethodGet, "/user/repos"},
{"GET", "/users/:user/repos"}, {http.MethodGet, "/users/:user/repos"},
{"GET", "/orgs/:org/repos"}, {http.MethodGet, "/orgs/:org/repos"},
{"GET", "/repositories"}, {http.MethodGet, "/repositories"},
{"POST", "/user/repos"}, {http.MethodPost, "/user/repos"},
{"POST", "/orgs/:org/repos"}, {http.MethodPost, "/orgs/:org/repos"},
{"GET", "/repos/:owner/:repo"}, {http.MethodGet, "/repos/:owner/:repo"},
//{"PATCH", "/repos/:owner/:repo"}, //{http.MethodPatch, "/repos/:owner/:repo"},
{"GET", "/repos/:owner/:repo/contributors"}, {http.MethodGet, "/repos/:owner/:repo/contributors"},
{"GET", "/repos/:owner/:repo/languages"}, {http.MethodGet, "/repos/:owner/:repo/languages"},
{"GET", "/repos/:owner/:repo/teams"}, {http.MethodGet, "/repos/:owner/:repo/teams"},
{"GET", "/repos/:owner/:repo/tags"}, {http.MethodGet, "/repos/:owner/:repo/tags"},
{"GET", "/repos/:owner/:repo/branches"}, {http.MethodGet, "/repos/:owner/:repo/branches"},
{"GET", "/repos/:owner/:repo/branches/:branch"}, {http.MethodGet, "/repos/:owner/:repo/branches/:branch"},
{"DELETE", "/repos/:owner/:repo"}, {http.MethodDelete, "/repos/:owner/:repo"},
{"GET", "/repos/:owner/:repo/collaborators"}, {http.MethodGet, "/repos/:owner/:repo/collaborators"},
{"GET", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodGet, "/repos/:owner/:repo/collaborators/:user"},
{"PUT", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodPut, "/repos/:owner/:repo/collaborators/:user"},
{"DELETE", "/repos/:owner/:repo/collaborators/:user"}, {http.MethodDelete, "/repos/:owner/:repo/collaborators/:user"},
{"GET", "/repos/:owner/:repo/comments"}, {http.MethodGet, "/repos/:owner/:repo/comments"},
{"GET", "/repos/:owner/:repo/commits/:sha/comments"}, {http.MethodGet, "/repos/:owner/:repo/commits/:sha/comments"},
{"POST", "/repos/:owner/:repo/commits/:sha/comments"}, {http.MethodPost, "/repos/:owner/:repo/commits/:sha/comments"},
{"GET", "/repos/:owner/:repo/comments/:id"}, {http.MethodGet, "/repos/:owner/:repo/comments/:id"},
//{"PATCH", "/repos/:owner/:repo/comments/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/comments/:id"},
{"DELETE", "/repos/:owner/:repo/comments/:id"}, {http.MethodDelete, "/repos/:owner/:repo/comments/:id"},
{"GET", "/repos/:owner/:repo/commits"}, {http.MethodGet, "/repos/:owner/:repo/commits"},
{"GET", "/repos/:owner/:repo/commits/:sha"}, {http.MethodGet, "/repos/:owner/:repo/commits/:sha"},
{"GET", "/repos/:owner/:repo/readme"}, {http.MethodGet, "/repos/:owner/:repo/readme"},
//{"GET", "/repos/:owner/:repo/contents/*path"}, //{http.MethodGet, "/repos/:owner/:repo/contents/*path"},
//{"PUT", "/repos/:owner/:repo/contents/*path"}, //{http.MethodPut, "/repos/:owner/:repo/contents/*path"},
//{"DELETE", "/repos/:owner/:repo/contents/*path"}, //{http.MethodDelete, "/repos/:owner/:repo/contents/*path"},
//{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, //{http.MethodGet, "/repos/:owner/:repo/:archive_format/:ref"},
{"GET", "/repos/:owner/:repo/keys"}, {http.MethodGet, "/repos/:owner/:repo/keys"},
{"GET", "/repos/:owner/:repo/keys/:id"}, {http.MethodGet, "/repos/:owner/:repo/keys/:id"},
{"POST", "/repos/:owner/:repo/keys"}, {http.MethodPost, "/repos/:owner/:repo/keys"},
//{"PATCH", "/repos/:owner/:repo/keys/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/keys/:id"},
{"DELETE", "/repos/:owner/:repo/keys/:id"}, {http.MethodDelete, "/repos/:owner/:repo/keys/:id"},
{"GET", "/repos/:owner/:repo/downloads"}, {http.MethodGet, "/repos/:owner/:repo/downloads"},
{"GET", "/repos/:owner/:repo/downloads/:id"}, {http.MethodGet, "/repos/:owner/:repo/downloads/:id"},
{"DELETE", "/repos/:owner/:repo/downloads/:id"}, {http.MethodDelete, "/repos/:owner/:repo/downloads/:id"},
{"GET", "/repos/:owner/:repo/forks"}, {http.MethodGet, "/repos/:owner/:repo/forks"},
{"POST", "/repos/:owner/:repo/forks"}, {http.MethodPost, "/repos/:owner/:repo/forks"},
{"GET", "/repos/:owner/:repo/hooks"}, {http.MethodGet, "/repos/:owner/:repo/hooks"},
{"GET", "/repos/:owner/:repo/hooks/:id"}, {http.MethodGet, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/hooks"}, {http.MethodPost, "/repos/:owner/:repo/hooks"},
//{"PATCH", "/repos/:owner/:repo/hooks/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/hooks/:id/tests"}, {http.MethodPost, "/repos/:owner/:repo/hooks/:id/tests"},
{"DELETE", "/repos/:owner/:repo/hooks/:id"}, {http.MethodDelete, "/repos/:owner/:repo/hooks/:id"},
{"POST", "/repos/:owner/:repo/merges"}, {http.MethodPost, "/repos/:owner/:repo/merges"},
{"GET", "/repos/:owner/:repo/releases"}, {http.MethodGet, "/repos/:owner/:repo/releases"},
{"GET", "/repos/:owner/:repo/releases/:id"}, {http.MethodGet, "/repos/:owner/:repo/releases/:id"},
{"POST", "/repos/:owner/:repo/releases"}, {http.MethodPost, "/repos/:owner/:repo/releases"},
//{"PATCH", "/repos/:owner/:repo/releases/:id"}, //{http.MethodPatch, "/repos/:owner/:repo/releases/:id"},
{"DELETE", "/repos/:owner/:repo/releases/:id"}, {http.MethodDelete, "/repos/:owner/:repo/releases/:id"},
{"GET", "/repos/:owner/:repo/releases/:id/assets"}, {http.MethodGet, "/repos/:owner/:repo/releases/:id/assets"},
{"GET", "/repos/:owner/:repo/stats/contributors"}, {http.MethodGet, "/repos/:owner/:repo/stats/contributors"},
{"GET", "/repos/:owner/:repo/stats/commit_activity"}, {http.MethodGet, "/repos/:owner/:repo/stats/commit_activity"},
{"GET", "/repos/:owner/:repo/stats/code_frequency"}, {http.MethodGet, "/repos/:owner/:repo/stats/code_frequency"},
{"GET", "/repos/:owner/:repo/stats/participation"}, {http.MethodGet, "/repos/:owner/:repo/stats/participation"},
{"GET", "/repos/:owner/:repo/stats/punch_card"}, {http.MethodGet, "/repos/:owner/:repo/stats/punch_card"},
{"GET", "/repos/:owner/:repo/statuses/:ref"}, {http.MethodGet, "/repos/:owner/:repo/statuses/:ref"},
{"POST", "/repos/:owner/:repo/statuses/:ref"}, {http.MethodPost, "/repos/:owner/:repo/statuses/:ref"},
// Search // Search
{"GET", "/search/repositories"}, {http.MethodGet, "/search/repositories"},
{"GET", "/search/code"}, {http.MethodGet, "/search/code"},
{"GET", "/search/issues"}, {http.MethodGet, "/search/issues"},
{"GET", "/search/users"}, {http.MethodGet, "/search/users"},
{"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, {http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword"},
{"GET", "/legacy/repos/search/:keyword"}, {http.MethodGet, "/legacy/repos/search/:keyword"},
{"GET", "/legacy/user/search/:keyword"}, {http.MethodGet, "/legacy/user/search/:keyword"},
{"GET", "/legacy/user/email/:email"}, {http.MethodGet, "/legacy/user/email/:email"},
// Users // Users
{"GET", "/users/:user"}, {http.MethodGet, "/users/:user"},
{"GET", "/user"}, {http.MethodGet, "/user"},
//{"PATCH", "/user"}, //{http.MethodPatch, "/user"},
{"GET", "/users"}, {http.MethodGet, "/users"},
{"GET", "/user/emails"}, {http.MethodGet, "/user/emails"},
{"POST", "/user/emails"}, {http.MethodPost, "/user/emails"},
{"DELETE", "/user/emails"}, {http.MethodDelete, "/user/emails"},
{"GET", "/users/:user/followers"}, {http.MethodGet, "/users/:user/followers"},
{"GET", "/user/followers"}, {http.MethodGet, "/user/followers"},
{"GET", "/users/:user/following"}, {http.MethodGet, "/users/:user/following"},
{"GET", "/user/following"}, {http.MethodGet, "/user/following"},
{"GET", "/user/following/:user"}, {http.MethodGet, "/user/following/:user"},
{"GET", "/users/:user/following/:target_user"}, {http.MethodGet, "/users/:user/following/:target_user"},
{"PUT", "/user/following/:user"}, {http.MethodPut, "/user/following/:user"},
{"DELETE", "/user/following/:user"}, {http.MethodDelete, "/user/following/:user"},
{"GET", "/users/:user/keys"}, {http.MethodGet, "/users/:user/keys"},
{"GET", "/user/keys"}, {http.MethodGet, "/user/keys"},
{"GET", "/user/keys/:id"}, {http.MethodGet, "/user/keys/:id"},
{"POST", "/user/keys"}, {http.MethodPost, "/user/keys"},
//{"PATCH", "/user/keys/:id"}, //{http.MethodPatch, "/user/keys/:id"},
{"DELETE", "/user/keys/:id"}, {http.MethodDelete, "/user/keys/:id"},
} }
func TestShouldBindUri(t *testing.T) { func TestShouldBindUri(t *testing.T) {
@ -291,18 +291,18 @@ func TestShouldBindUri(t *testing.T) {
type Person struct { type Person struct {
Name string `uri:"name" binding:"required"` Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"` ID string `uri:"id" binding:"required"`
} }
router.Handle("GET", "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.ShouldBindUri(&person)) assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name) assert.True(t, person.Name != "")
assert.True(t, "" != person.Id) assert.True(t, person.ID != "")
c.String(http.StatusOK, "ShouldBindUri test OK") c.String(http.StatusOK, "ShouldBindUri test OK")
}) })
path, _ := exampleFromPath("/rest/:name/:id") path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path) w := PerformRequest(router, http.MethodGet, path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String()) assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -313,18 +313,18 @@ func TestBindUri(t *testing.T) {
type Person struct { type Person struct {
Name string `uri:"name" binding:"required"` Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"` ID string `uri:"id" binding:"required"`
} }
router.Handle("GET", "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.BindUri(&person)) assert.NoError(t, c.BindUri(&person))
assert.True(t, "" != person.Name) assert.True(t, person.Name != "")
assert.True(t, "" != person.Id) assert.True(t, person.ID != "")
c.String(http.StatusOK, "BindUri test OK") c.String(http.StatusOK, "BindUri test OK")
}) })
path, _ := exampleFromPath("/rest/:name/:id") path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path) w := PerformRequest(router, http.MethodGet, path)
assert.Equal(t, "BindUri test OK", w.Body.String()) assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
} }
@ -336,13 +336,13 @@ func TestBindUriError(t *testing.T) {
type Member struct { type Member struct {
Number string `uri:"num" binding:"required,uuid"` Number string `uri:"num" binding:"required,uuid"`
} }
router.Handle("GET", "/new/rest/:num", func(c *Context) { router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member var m Member
assert.Error(t, c.BindUri(&m)) assert.Error(t, c.BindUri(&m))
}) })
path1, _ := exampleFromPath("/new/rest/:num") path1, _ := exampleFromPath("/new/rest/:num")
w1 := performRequest(router, "GET", path1) w1 := PerformRequest(router, http.MethodGet, path1)
assert.Equal(t, http.StatusBadRequest, w1.Code) assert.Equal(t, http.StatusBadRequest, w1.Code)
} }
@ -358,7 +358,7 @@ func TestRaceContextCopy(t *testing.T) {
go readWriteKeys(c.Copy()) go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics") c.String(http.StatusOK, "run OK, no panics")
}) })
w := performRequest(router, "GET", "/test/copy/race") w := PerformRequest(router, http.MethodGet, "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String()) assert.Equal(t, "run OK, no panics", w.Body.String())
} }
@ -389,7 +389,7 @@ func TestGithubAPI(t *testing.T) {
for _, route := range githubAPI { for _, route := range githubAPI {
path, values := exampleFromPath(route.path) path, values := exampleFromPath(route.path)
w := performRequest(router, route.method, path) w := PerformRequest(router, route.method, path)
// TEST // TEST
assert.Contains(t, w.Body.String(), "\"status\":\"good\"") assert.Contains(t, w.Body.String(), "\"status\":\"good\"")
@ -401,7 +401,7 @@ func TestGithubAPI(t *testing.T) {
} }
func exampleFromPath(path string) (string, Params) { func exampleFromPath(path string) (string, Params) {
output := new(bytes.Buffer) output := new(strings.Builder)
params := make(Params, 0, 6) params := make(Params, 0, 6)
start := -1 start := -1
for i, c := range path { for i, c := range path {
@ -438,7 +438,7 @@ func exampleFromPath(path string) (string, Params) {
func BenchmarkGithub(b *testing.B) { func BenchmarkGithub(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
runRequest(b, router, "GET", "/legacy/issues/search/:owner/:repository/:state/:keyword") runRequest(b, router, http.MethodGet, "/legacy/issues/search/:owner/:repository/:state/:keyword")
} }
func BenchmarkParallelGithub(b *testing.B) { func BenchmarkParallelGithub(b *testing.B) {
@ -446,7 +446,7 @@ func BenchmarkParallelGithub(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer. // Each goroutine has its own bytes.Buffer.
@ -462,7 +462,7 @@ func BenchmarkParallelGithubDefault(b *testing.B) {
router := New() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest(http.MethodPost, "/repos/manucorporat/sse/git/blobs", nil)
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
// Each goroutine has its own bytes.Buffer. // Each goroutine has its own bytes.Buffer.

46
go.mod
View File

@ -1,18 +1,38 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.12 go 1.20
require ( require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 github.com/bytedance/sonic v1.11.6
github.com/golang/protobuf v1.3.0 github.com/gin-contrib/sse v0.1.0
github.com/json-iterator/go v1.1.5 github.com/go-playground/validator/v10 v10.20.0
github.com/mattn/go-isatty v0.0.6 github.com/goccy/go-json v0.10.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/json-iterator/go v1.1.12
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/mattn/go-isatty v0.0.20
github.com/stretchr/testify v1.3.0 github.com/pelletier/go-toml/v2 v2.2.2
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 github.com/stretchr/testify v1.9.0
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 github.com/ugorji/go/codec v1.2.12
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect golang.org/x/net v0.25.0
gopkg.in/go-playground/validator.v8 v8.18.2 google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
) )

105
go.sum
View File

@ -1,39 +1,84 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

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

@ -0,0 +1,99 @@
// 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 (
"bytes"
"math/rand"
"strings"
"testing"
"time"
)
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
var testBytes = []byte(testString)
func rawBytesToStr(b []byte) string {
return string(b)
}
func rawStrToBytes(s string) []byte {
return []byte(s)
}
// go test -v
func TestBytesToString(t *testing.T) {
data := make([]byte, 1024)
for i := 0; i < 100; i++ {
rand.Read(data)
if rawBytesToStr(data) != BytesToString(data) {
t.Fatal("don't match")
}
}
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
func RandStringBytesMaskImprSrcSB(n int) string {
sb := strings.Builder{}
sb.Grow(n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
sb.WriteByte(letterBytes[idx])
i--
}
cache >>= letterIdxBits
remain--
}
return sb.String()
}
func TestStringToBytes(t *testing.T) {
for i := 0; i < 100; i++ {
s := RandStringBytesMaskImprSrcSB(64)
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
t.Fatal("don't match")
}
}
}
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawBytesToStr(testBytes)
}
}
func BenchmarkBytesConvBytesToStr(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesToString(testBytes)
}
}
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawStrToBytes(testString)
}
}
func BenchmarkBytesConvStrToBytes(b *testing.B) {
for i := 0; i < b.N; i++ {
StringToBytes(testString)
}
}

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

@ -1,8 +1,8 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved. // Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !jsoniter //go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
package json package json

View File

@ -1,12 +1,12 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved. // Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build jsoniter //go:build jsoniter
package json package json
import "github.com/json-iterator/go" import jsoniter "github.com/json-iterator/go"
var ( var (
json = jsoniter.ConfigCompatibleWithStandardLibrary 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
)

117
logger.go
View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -22,18 +22,19 @@ const (
forceColor forceColor
) )
var ( const (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) green = "\033[97;42m"
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) white = "\033[90;47m"
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) yellow = "\033[90;43m"
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) red = "\033[97;41m"
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) blue = "\033[97;44m"
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) magenta = "\033[97;45m"
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) cyan = "\033[97;46m"
reset = string([]byte{27, 91, 48, 109}) reset = "\033[0m"
consoleColorMode = autoColor
) )
var consoleColorMode = autoColor
// LoggerConfig defines the config for Logger middleware. // LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct { type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter // Optional. Default value is gin.defaultLogFormatter
@ -43,11 +44,18 @@ type LoggerConfig struct {
// Optional. Default value is gin.DefaultWriter. // Optional. Default value is gin.DefaultWriter.
Output io.Writer 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. // Optional.
SkipPaths []string SkipPaths []string
// Skip is a Skipper that indicates which logs should not be written.
// Optional.
Skip Skipper
} }
// Skipper is a function to skip logs based on provided Context
type Skipper func(c *Context) bool
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter // LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
type LogFormatter func(params LogFormatterParams) string type LogFormatter func(params LogFormatterParams) string
@ -69,12 +77,12 @@ type LogFormatterParams struct {
Path string Path string
// ErrorMessage is set if error has occurred in processing the request. // ErrorMessage is set if error has occurred in processing the request.
ErrorMessage string 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 isTerm bool
// BodySize is the size of the Response Body // BodySize is the size of the Response Body
BodySize int BodySize int
// Keys are the keys set on the request's context. // 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. // StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
@ -82,6 +90,8 @@ func (p *LogFormatterParams) StatusCodeColor() string {
code := p.StatusCode code := p.StatusCode
switch { switch {
case code >= http.StatusContinue && code < http.StatusOK:
return white
case code >= http.StatusOK && code < http.StatusMultipleChoices: case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green return green
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
@ -98,19 +108,19 @@ func (p *LogFormatterParams) MethodColor() string {
method := p.Method method := p.Method
switch method { switch method {
case "GET": case http.MethodGet:
return blue return blue
case "POST": case http.MethodPost:
return cyan return cyan
case "PUT": case http.MethodPut:
return yellow return yellow
case "DELETE": case http.MethodDelete:
return red return red
case "PATCH": case http.MethodPatch:
return green return green
case "HEAD": case http.MethodHead:
return magenta return magenta
case "OPTIONS": case http.MethodOptions:
return white return white
default: default:
return reset return reset
@ -137,10 +147,9 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
} }
if param.Latency > time.Minute { if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way param.Latency = param.Latency.Truncate(time.Second)
param.Latency = param.Latency - param.Latency%time.Second
} }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", 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"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor, statusColor, param.StatusCode, resetColor,
param.Latency, param.Latency,
@ -161,12 +170,12 @@ func ForceConsoleColor() {
consoleColorMode = forceColor consoleColorMode = forceColor
} }
// ErrorLogger returns a handlerfunc for any error type. // ErrorLogger returns a HandlerFunc for any error type.
func ErrorLogger() HandlerFunc { func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAny) 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 { func ErrorLoggerT(typ ErrorType) HandlerFunc {
return func(c *Context) { return func(c *Context) {
c.Next() c.Next()
@ -178,7 +187,7 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
} }
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. // 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 { func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{}) return LoggerWithConfig(LoggerConfig{})
} }
@ -239,32 +248,34 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
// Process request // Process request
c.Next() c.Next()
// Log only when path is not being skipped // Log only when it is not being skipped
if _, ok := skip[path]; !ok { if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) {
param := LogFormatterParams{ return
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
fmt.Fprint(out, formatter(param))
} }
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
fmt.Fprint(out, formatter(param))
} }
} }

View File

@ -1,14 +1,14 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"testing" "testing"
"time" "time"
@ -20,7 +20,7 @@ func init() {
} }
func TestLogger(t *testing.T) { func TestLogger(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
router.Use(LoggerWithWriter(buffer)) router.Use(LoggerWithWriter(buffer))
router.GET("/example", func(c *Context) {}) router.GET("/example", func(c *Context) {})
@ -31,7 +31,7 @@ func TestLogger(t *testing.T) {
router.HEAD("/example", func(c *Context) {}) router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/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(), "200")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") 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 // like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go. // than individual functions. Im not sure where these should go.
buffer.Reset() buffer.Reset()
performRequest(router, "POST", "/example") PerformRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST") assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "PUT", "/example") PerformRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT") assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "DELETE", "/example") PerformRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE") assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "PATCH", "/example") PerformRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH") assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "HEAD", "/example") PerformRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD") assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "OPTIONS", "/example") PerformRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS") assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "GET", "/notfound") PerformRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
} }
func TestLoggerWithConfig(t *testing.T) { func TestLoggerWithConfig(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer})) router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
router.GET("/example", func(c *Context) {}) router.GET("/example", func(c *Context) {})
@ -95,7 +95,7 @@ func TestLoggerWithConfig(t *testing.T) {
router.HEAD("/example", func(c *Context) {}) router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/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(), "200")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") 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 // like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go. // than individual functions. Im not sure where these should go.
buffer.Reset() buffer.Reset()
performRequest(router, "POST", "/example") PerformRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST") assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "PUT", "/example") PerformRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT") assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "DELETE", "/example") PerformRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE") assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "PATCH", "/example") PerformRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH") assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "HEAD", "/example") PerformRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD") assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "OPTIONS", "/example") PerformRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS") assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
performRequest(router, "GET", "/notfound") PerformRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
} }
func TestLoggerWithFormatter(t *testing.T) { func TestLoggerWithFormatter(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
d := DefaultWriter d := DefaultWriter
DefaultWriter = buffer DefaultWriter = buffer
@ -158,7 +158,7 @@ func TestLoggerWithFormatter(t *testing.T) {
router := New() router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string { router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s", return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode, param.StatusCode,
param.Latency, param.Latency,
@ -169,7 +169,7 @@ func TestLoggerWithFormatter(t *testing.T) {
) )
})) }))
router.GET("/example", func(c *Context) {}) router.GET("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100") PerformRequest(router, "GET", "/example?a=100")
// output test // output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]") assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
@ -181,10 +181,12 @@ func TestLoggerWithFormatter(t *testing.T) {
func TestLoggerWithConfigFormatting(t *testing.T) { func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams var gotParam LogFormatterParams
var gotKeys map[string]interface{} var gotKeys map[string]any
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
router.Use(LoggerWithConfig(LoggerConfig{ router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer, Output: buffer,
Formatter: func(param LogFormatterParams) string { Formatter: func(param LogFormatterParams) string {
@ -206,8 +208,9 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
// set dummy ClientIP // set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20") c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
gotKeys = c.Keys gotKeys = c.Keys
time.Sleep(time.Millisecond)
}) })
performRequest(router, "GET", "/example?a=100") PerformRequest(router, "GET", "/example?a=100")
// output test // output test
assert.Contains(t, buffer.String(), "[FORMATTER 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.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage) assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys) assert.Equal(t, gotKeys, gotParam.Keys)
} }
func TestDefaultLogFormatter(t *testing.T) { func TestDefaultLogFormatter(t *testing.T) {
@ -275,12 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
isTerm: false, isTerm: false,
} }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
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))
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) { func TestColorForMethod(t *testing.T) {
@ -291,14 +292,14 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor() return p.MethodColor()
} }
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue") assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan") assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow") assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red") assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green") assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white") assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color") assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
} }
func TestColorForStatus(t *testing.T) { func TestColorForStatus(t *testing.T) {
@ -309,10 +310,11 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor() return p.StatusCodeColor()
} }
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, white, colorForStatus(http.StatusContinue), "1xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red") assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, red, colorForStatus(2), "other things should be red")
} }
func TestResetColor(t *testing.T) { func TestResetColor(t *testing.T) {
@ -357,46 +359,46 @@ func TestErrorLogger(t *testing.T) {
router := New() router := New()
router.Use(ErrorLogger()) router.Use(ErrorLogger())
router.GET("/error", func(c *Context) { 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) { 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) { 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!") 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, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String()) 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, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String()) 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, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestLoggerWithWriterSkippingPaths(t *testing.T) { func TestLoggerWithWriterSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
router.Use(LoggerWithWriter(buffer, "/skipped")) router.Use(LoggerWithWriter(buffer, "/skipped"))
router.GET("/logged", func(c *Context) {}) router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {}) router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged") PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
performRequest(router, "GET", "/skipped") PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
func TestLoggerWithConfigSkippingPaths(t *testing.T) { func TestLoggerWithConfigSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
router.Use(LoggerWithConfig(LoggerConfig{ router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer, Output: buffer,
@ -405,11 +407,31 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {}) router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {}) router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged") PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
performRequest(router, "GET", "/skipped") PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestLoggerWithConfigSkipper(t *testing.T) {
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Skip: func(c *Context) bool {
return c.Writer.Status() == http.StatusNoContent
},
}))
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
PerformRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
signature += " X " signature += " X "
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -118,7 +118,10 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
func TestMiddlewareNoMethodDisabled(t *testing.T) { func TestMiddlewareNoMethodDisabled(t *testing.T) {
signature := "" signature := ""
router := New() router := New()
// NoMethod disabled
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
router.Use(func(c *Context) { router.Use(func(c *Context) {
signature += "A" signature += "A"
c.Next() c.Next()
@ -144,8 +147,9 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
router.POST("/", func(c *Context) { router.POST("/", func(c *Context) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
@ -171,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
@ -186,14 +190,13 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
c.Next() c.Next()
c.AbortWithStatus(http.StatusGone) c.AbortWithStatus(http.StatusGone)
signature += "B" signature += "B"
}) })
router.GET("/", func(c *Context) { router.GET("/", func(c *Context) {
signature += "C" signature += "C"
c.Next() c.Next()
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusGone, w.Code) assert.Equal(t, http.StatusGone, w.Code)
@ -208,7 +211,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New() router := New()
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "A" 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) { router.Use(func(context *Context) {
signature += "B" signature += "B"
@ -216,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C" signature += "C"
}) })
// RUN // RUN
w := performRequest(router, "GET", "/") w := PerformRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) 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, 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)) 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))

32
mode.go
View File

@ -1,10 +1,11 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"flag"
"io" "io"
"os" "os"
"sync/atomic" "sync/atomic"
@ -23,6 +24,7 @@ const (
// TestMode indicates gin mode is test. // TestMode indicates gin mode is test.
TestMode = "test" TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
releaseCode releaseCode
@ -34,8 +36,9 @@ const (
// Note that both Logger and Recovery provides custom ways to configure their // Note that both Logger and Recovery provides custom ways to configure their
// output io.Writer. // output io.Writer.
// To support coloring in Windows use: // To support coloring in Windows use:
// import "github.com/mattn/go-colorable" //
// gin.DefaultWriter = colorable.NewColorableStdout() // import "github.com/mattn/go-colorable"
// gin.DefaultWriter = colorable.NewColorableStdout()
var DefaultWriter io.Writer = os.Stdout var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors // DefaultErrorWriter is the default io.Writer used by Gin to debug errors
@ -51,6 +54,14 @@ func init() {
// SetMode sets gin mode according to input string. // SetMode sets gin mode according to input string.
func SetMode(value string) { func SetMode(value string) {
if value == "" {
if flag.Lookup("test.v") != nil {
value = TestMode
} else {
value = DebugMode
}
}
switch value { switch value {
case DebugMode, "": case DebugMode, "":
atomic.StoreInt32(&ginMode, debugCode) atomic.StoreInt32(&ginMode, debugCode)
@ -59,10 +70,7 @@ func SetMode(value string) {
case TestMode: case TestMode:
atomic.StoreInt32(&ginMode, testCode) atomic.StoreInt32(&ginMode, testCode)
default: default:
panic("gin mode unknown: " + value) panic("gin mode unknown: " + value + " (available mode: debug release test)")
}
if value == "" {
value = DebugMode
} }
modeName.Store(value) modeName.Store(value)
} }
@ -72,13 +80,19 @@ func DisableBindValidation() {
binding.Validator = nil binding.Validator = nil
} }
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to // EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumber to
// call the UseNumber method on the JSON Decoder instance. // call the UseNumber method on the JSON Decoder instance.
func EnableJsonDecoderUseNumber() { func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true binding.EnableDecoderUseNumber = true
} }
// Mode returns currently gin mode. // EnableJsonDecoderDisallowUnknownFields sets true for binding.EnableDecoderDisallowUnknownFields to
// call the DisallowUnknownFields method on the JSON Decoder instance.
func EnableJsonDecoderDisallowUnknownFields() {
binding.EnableDecoderDisallowUnknownFields = true
}
// Mode returns current gin mode.
func Mode() string { func Mode() string {
return modeName.Load().(string) return modeName.Load().(string)
} }

View File

@ -1,10 +1,11 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"flag"
"os" "os"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -41,8 +42,22 @@ func TestSetMode(t *testing.T) {
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })
} }
func TestDisableBindValidation(t *testing.T) {
v := binding.Validator
assert.NotNil(t, binding.Validator)
DisableBindValidation()
assert.Nil(t, binding.Validator)
binding.Validator = v
}
func TestEnableJsonDecoderUseNumber(t *testing.T) { func TestEnableJsonDecoderUseNumber(t *testing.T) {
assert.False(t, binding.EnableDecoderUseNumber) assert.False(t, binding.EnableDecoderUseNumber)
EnableJsonDecoderUseNumber() EnableJsonDecoderUseNumber()
assert.True(t, binding.EnableDecoderUseNumber) assert.True(t, binding.EnableDecoderUseNumber)
} }
func TestEnableJsonDecoderDisallowUnknownFields(t *testing.T) {
assert.False(t, binding.EnableDecoderDisallowUnknownFields)
EnableJsonDecoderDisallowUnknownFields()
assert.True(t, binding.EnableDecoderDisallowUnknownFields)
}

69
path.go
View File

@ -10,22 +10,26 @@ package gin
// //
// The following rules are applied iteratively until no further processing can // The following rules are applied iteratively until no further processing can
// be done: // be done:
// 1. Replace multiple slashes with a single slash. // 1. Replace multiple slashes with a single slash.
// 2. Eliminate each . path name element (the current directory). // 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory) // 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it. // along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path: // 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path. // that is, replace "/.." by "/" at the beginning of a path.
// //
// If the result of this process is an empty string, "/" is returned. // If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string { func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/" // Turn empty string into "/"
if p == "" { if p == "" {
return "/" return "/"
} }
// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)
n := len(p) n := len(p)
var buf []byte
// Invariants: // Invariants:
// reading from path; r is index of next byte to process. // reading from path; r is index of next byte to process.
@ -37,15 +41,21 @@ func cleanPath(p string) string {
if p[0] != '/' { if p[0] != '/' {
r = 0 r = 0
buf = make([]byte, n+1)
if n+1 > stackBufSize {
buf = make([]byte, n+1)
} else {
buf = buf[:n+1]
}
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 1 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this // gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n { for r < n {
switch { switch {
@ -69,7 +79,7 @@ func cleanPath(p string) string {
// can backtrack // can backtrack
w-- w--
if buf == nil { if len(buf) == 0 {
for w > 1 && p[w] != '/' { for w > 1 && p[w] != '/' {
w-- w--
} }
@ -81,14 +91,14 @@ func cleanPath(p string) string {
} }
default: default:
// real path element. // Real path element.
// add slash if needed // Add slash if needed
if w > 1 { if w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
// copy element // Copy element
for r < n && p[r] != '/' { for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r]) bufApp(&buf, p, w, p[r])
w++ w++
@ -97,27 +107,44 @@ func cleanPath(p string) string {
} }
} }
// re-append trailing slash // Re-append trailing slash
if trailing && w > 1 { if trailing && w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
if buf == nil { // If the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise return a new string from the buffer.
if len(buf) == 0 {
return p[:w] return p[:w]
} }
return string(buf[:w]) return string(buf[:w])
} }
// internal helper to lazily create a buffer if necessary. // Internal helper to lazily create a buffer if necessary.
// Calls to this function get inlined.
func bufApp(buf *[]byte, s string, w int, c byte) { func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil { b := *buf
if len(b) == 0 {
// No modification of the original string so far.
// If the next character is the same as in the original string, we do
// not yet have to allocate a buffer.
if s[w] == c { if s[w] == c {
return return
} }
*buf = make([]byte, len(s)) // Otherwise use either the stack buffer, if it is large enough, or
copy(*buf, s[:w]) // allocate a new buffer on the heap, and copy all previous characters.
length := len(s)
if length > cap(b) {
*buf = make([]byte, length)
} else {
*buf = (*buf)[:length]
}
b = *buf
copy(b, s[:w])
} }
(*buf)[w] = c b[w] = c
} }

View File

@ -7,14 +7,17 @@ package gin
import ( import (
"runtime" "runtime"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var cleanTests = []struct { type cleanPathTest struct {
path, result string path, result string
}{ }
var cleanTests = []cleanPathTest{
// Already clean // Already clean
{"/", "/"}, {"/", "/"},
{"/abc", "/abc"}, {"/abc", "/abc"},
@ -77,9 +80,9 @@ func TestPathCleanMallocs(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping malloc count in short mode") t.Skip("skipping malloc count in short mode")
} }
if runtime.GOMAXPROCS(0) > 1 { if runtime.GOMAXPROCS(0) > 1 {
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") t.Skip("skipping malloc count; GOMAXPROCS>1")
return
} }
for _, test := range cleanTests { for _, test := range cleanTests {
@ -87,3 +90,56 @@ func TestPathCleanMallocs(t *testing.T) {
assert.EqualValues(t, allocs, 0) assert.EqualValues(t, allocs, 0)
} }
} }
func BenchmarkPathClean(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}
func genLongPaths() (testPaths []cleanPathTest) {
for i := 1; i <= 1234; i++ {
ss := strings.Repeat("a", i)
correctPath := "/" + ss
testPaths = append(testPaths, cleanPathTest{
path: correctPath,
result: correctPath,
}, cleanPathTest{
path: ss,
result: correctPath,
}, cleanPathTest{
path: "//" + ss,
result: correctPath,
}, cleanPathTest{
path: "/" + ss + "/b/..",
result: correctPath,
})
}
return
}
func TestPathCleanLong(t *testing.T) {
cleanTests := genLongPaths()
for _, test := range cleanTests {
assert.Equal(t, test.result, cleanPath(test.path))
assert.Equal(t, test.result, cleanPath(test.result))
}
}
func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -6,9 +6,9 @@ package gin
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -26,13 +26,29 @@ var (
slash = []byte("/") 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. // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc { func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter) 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. // 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 var logger *log.Logger
if out != nil { if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) 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. // condition that warrants a panic stack trace.
var brokenPipe bool var brokenPipe bool
if ne, ok := err.(*net.OpError); ok { if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok { var se *os.SyscallError
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { 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 brokenPipe = true
} }
} }
@ -60,23 +79,23 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
headers[idx] = current[0] + ": *" headers[idx] = current[0] + ": *"
} }
} }
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset) logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() { } else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", 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 { } else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset) timeFormat(time.Now()), err, stack, reset)
} }
} }
// If the connection is dead, we can't write a status to it.
if brokenPipe { 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() c.Abort()
} else { } 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. // stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte { func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data 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. // 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) fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile { if file != lastFile {
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
continue continue
} }
@ -133,7 +156,7 @@ func function(pc uintptr) []byte {
// runtime/debug.*T·ptrmethod // runtime/debug.*T·ptrmethod
// and want // and want
// *T.ptrmethod // *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 // so first eliminate the path prefix
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
name = name[lastSlash+1:] name = name[lastSlash+1:]
@ -141,11 +164,11 @@ func function(pc uintptr) []byte {
if period := bytes.Index(name, dot); period >= 0 { if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:] name = name[period+1:]
} }
name = bytes.Replace(name, centerDot, dot, -1) name = bytes.ReplaceAll(name, centerDot, dot)
return name return name
} }
// timeFormat returns a customized time string for logger.
func timeFormat(t time.Time) string { func timeFormat(t time.Time) string {
var timeString = t.Format("2006/01/02 - 15:04:05") return t.Format("2006/01/02 - 15:04:05")
return timeString
} }

View File

@ -1,11 +1,10 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -18,7 +17,7 @@ import (
) )
func TestPanicClean(t *testing.T) { func TestPanicClean(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
password := "my-super-secret-password" password := "my-super-secret-password"
router.Use(RecoveryWithWriter(buffer)) router.Use(RecoveryWithWriter(buffer))
@ -27,7 +26,7 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := performRequest(router, "GET", "/recovery", w := PerformRequest(router, "GET", "/recovery",
header{ header{
Key: "Host", Key: "Host",
Value: "www.google.com", Value: "www.google.com",
@ -50,25 +49,25 @@ func TestPanicClean(t *testing.T) {
// TestPanicInHandler assert that panic has been recovered. // TestPanicInHandler assert that panic has been recovered.
func TestPanicInHandler(t *testing.T) { func TestPanicInHandler(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(strings.Builder)
router := New() router := New()
router.Use(RecoveryWithWriter(buffer)) router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(_ *Context) { router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") 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") assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = performRequest(router, "GET", "/recovery") w = PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -85,21 +84,21 @@ func TestPanicWithAbort(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
} }
func TestSource(t *testing.T) { func TestSource(t *testing.T) {
bs := source(nil, 0) bs := source(nil, 0)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, dunno, bs)
in := [][]byte{ in := [][]byte{
[]byte("Hello world."), []byte("Hello world."),
[]byte("Hi, gin.."), []byte("Hi, gin.."),
} }
bs = source(in, 10) bs = source(in, 10)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, dunno, bs)
bs = source(in, 1) bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs) assert.Equal(t, []byte("Hello world."), bs)
@ -107,7 +106,7 @@ func TestSource(t *testing.T) {
func TestFunction(t *testing.T) { func TestFunction(t *testing.T) {
bs := function(1) bs := function(1)
assert.Equal(t, []byte("???"), bs) assert.Equal(t, dunno, bs)
} }
// TestPanicWithBrokenPipe asserts that recovery specifically handles // TestPanicWithBrokenPipe asserts that recovery specifically handles
@ -122,8 +121,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
for errno, expectMsg := range expectMsgs { for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) { t.Run(expectMsg, func(t *testing.T) {
var buf strings.Builder
var buf bytes.Buffer
router := New() router := New()
router.Use(RecoveryWithWriter(&buf)) router.Use(RecoveryWithWriter(&buf))
@ -137,10 +135,114 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e) panic(e)
}) })
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := PerformRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, expectCode, w.Code) assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg) 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

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -20,7 +20,7 @@ type Delims struct {
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug. // HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
type HTMLRender interface { type HTMLRender interface {
// Instance returns an HTML instance. // Instance returns an HTML instance.
Instance(string, interface{}) Render Instance(string, any) Render
} }
// HTMLProduction contains template reference and its delims. // HTMLProduction contains template reference and its delims.
@ -41,13 +41,13 @@ type HTMLDebug struct {
type HTML struct { type HTML struct {
Template *template.Template Template *template.Template
Name string Name string
Data interface{} Data any
} }
var htmlContentType = []string{"text/html; charset=utf-8"} var htmlContentType = []string{"text/html; charset=utf-8"}
// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface. // 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{ return HTML{
Template: r.Template, Template: r.Template,
Name: name, 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. // 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{ return HTML{
Template: r.loadTemplate(), Template: r.loadTemplate(),
Name: name, Name: name,

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -10,54 +10,51 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
// JSON contains the given interface object. // JSON contains the given interface object.
type JSON struct { type JSON struct {
Data interface{} Data any
} }
// IndentedJSON contains the given interface object. // IndentedJSON contains the given interface object.
type IndentedJSON struct { type IndentedJSON struct {
Data interface{} Data any
} }
// SecureJSON contains the given interface object and its prefix. // SecureJSON contains the given interface object and its prefix.
type SecureJSON struct { type SecureJSON struct {
Prefix string Prefix string
Data interface{} Data any
} }
// JsonpJSON contains the given interface object its callback. // JsonpJSON contains the given interface object its callback.
type JsonpJSON struct { type JsonpJSON struct {
Callback string Callback string
Data interface{} Data any
} }
// AsciiJSON contains the given interface object. // AsciiJSON contains the given interface object.
type AsciiJSON struct { type AsciiJSON struct {
Data interface{} Data any
} }
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string
// PureJSON contains the given interface object. // PureJSON contains the given interface object.
type PureJSON struct { type PureJSON struct {
Data interface{} Data any
} }
var jsonContentType = []string{"application/json; charset=utf-8"} var (
var jsonpContentType = []string{"application/javascript; charset=utf-8"} jsonContentType = []string{"application/json; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"} jsonpContentType = []string{"application/javascript; charset=utf-8"}
jsonASCIIContentType = []string{"application/json"}
)
// Render (JSON) writes data with custom ContentType. // Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) error {
if err = WriteJSON(w, r.Data); err != nil { return WriteJSON(w, r.Data)
panic(err)
}
return
} }
// WriteContentType (JSON) writes JSON ContentType. // WriteContentType (JSON) writes JSON ContentType.
@ -66,7 +63,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
} }
// WriteJSON marshals the given interface object and writes it with custom ContentType. // 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) writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj) jsonBytes, err := json.Marshal(obj)
if err != nil { if err != nil {
@ -100,9 +97,9 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
return err return err
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
_, err = w.Write([]byte(r.Prefix)) bytesconv.StringToBytes("]")) {
if err != nil { if _, err = w.Write(bytesconv.StringToBytes(r.Prefix)); err != nil {
return err return err
} }
} }
@ -129,20 +126,19 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
} }
callback := template.JSEscapeString(r.Callback) callback := template.JSEscapeString(r.Callback)
_, err = w.Write([]byte(callback)) if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil {
if err != nil {
return err return err
} }
_, err = w.Write([]byte("("))
if err != nil { if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil {
return err return err
} }
_, err = w.Write(ret)
if err != nil { if _, err = w.Write(ret); err != nil {
return err return err
} }
_, err = w.Write([]byte(")"))
if err != nil { if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil {
return err return err
} }
@ -163,7 +159,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
} }
var buffer bytes.Buffer var buffer bytes.Buffer
for _, r := range string(ret) { for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r) cvt := string(r)
if r >= 128 { if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r)) cvt = fmt.Sprintf("\\u%04x", int64(r))
@ -177,7 +173,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
// WriteContentType (AsciiJSON) writes JSON ContentType. // WriteContentType (AsciiJSON) writes JSON ContentType.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) { func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType) writeContentType(w, jsonASCIIContentType)
} }
// Render (PureJSON) writes custom ContentType and encodes the given interface object. // Render (PureJSON) writes custom ContentType and encodes the given interface object.

View File

@ -1,7 +1,9 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved. // Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack
package render package render
import ( import (
@ -10,9 +12,15 @@ import (
"github.com/ugorji/go/codec" "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. // MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data interface{} Data any
} }
var msgpackContentType = []string{"application/msgpack; charset=utf-8"} var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
@ -28,7 +36,7 @@ func (r MsgPack) Render(w http.ResponseWriter) error {
} }
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object. // 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) writeContentType(w, msgpackContentType)
var mh codec.MsgpackHandle var mh codec.MsgpackHandle
return codec.NewEncoder(w, &mh).Encode(obj) return codec.NewEncoder(w, &mh).Encode(obj)

View File

@ -1,4 +1,4 @@
// Copyright 2018 Gin Core Team. All rights reserved. // Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,12 +7,12 @@ package render
import ( import (
"net/http" "net/http"
"github.com/golang/protobuf/proto" "google.golang.org/protobuf/proto"
) )
// ProtoBuf contains the given interface object. // ProtoBuf contains the given interface object.
type ProtoBuf struct { type ProtoBuf struct {
Data interface{} Data any
} }
var protobufContentType = []string{"application/x-protobuf"} var protobufContentType = []string{"application/x-protobuf"}

View File

@ -1,4 +1,4 @@
// Copyright 2018 Gin Core Team. All rights reserved. // Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -21,7 +21,12 @@ type Reader struct {
// Render (Reader) writes data with custom ContentType and headers. // Render (Reader) writes data with custom ContentType and headers.
func (r Reader) Render(w http.ResponseWriter) (err error) { func (r Reader) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) if r.ContentLength >= 0 {
if r.Headers == nil {
r.Headers = map[string]string{}
}
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
}
r.writeHeaders(w, r.Headers) r.writeHeaders(w, r.Headers)
_, err = io.Copy(w, r.Reader) _, err = io.Copy(w, r.Reader)
return return

23
render/reader_test.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2019 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/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestReaderRenderNoHeaders(t *testing.T) {
content := "test"
r := Reader{
ContentLength: int64(len(content)),
Reader: strings.NewReader(content),
}
err := r.Render(httptest.NewRecorder())
require.NoError(t, err)
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -15,22 +15,22 @@ type Render interface {
} }
var ( var (
_ Render = JSON{} _ Render = (*JSON)(nil)
_ Render = IndentedJSON{} _ Render = (*IndentedJSON)(nil)
_ Render = SecureJSON{} _ Render = (*SecureJSON)(nil)
_ Render = JsonpJSON{} _ Render = (*JsonpJSON)(nil)
_ Render = XML{} _ Render = (*XML)(nil)
_ Render = String{} _ Render = (*String)(nil)
_ Render = Redirect{} _ Render = (*Redirect)(nil)
_ Render = Data{} _ Render = (*Data)(nil)
_ Render = HTML{} _ Render = (*HTML)(nil)
_ HTMLRender = HTMLDebug{} _ HTMLRender = (*HTMLDebug)(nil)
_ HTMLRender = HTMLProduction{} _ HTMLRender = (*HTMLProduction)(nil)
_ Render = YAML{} _ Render = (*YAML)(nil)
_ Render = MsgPack{} _ Render = (*Reader)(nil)
_ Render = Reader{} _ Render = (*AsciiJSON)(nil)
_ Render = AsciiJSON{} _ Render = (*ProtoBuf)(nil)
_ Render = ProtoBuf{} _ Render = (*TOML)(nil)
) )
func writeContentType(w http.ResponseWriter, value []string) { func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -0,0 +1,43 @@
// Copyright 2014 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 !nomsgpack
package render
import (
"bytes"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
// TODO unit tests
// test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -1,57 +1,32 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package render package render
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"html/template" "html/template"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"github.com/golang/protobuf/proto" "github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
) )
// TODO unit tests // TODO unit tests
// test errors // test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderJSON(t *testing.T) { func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]any{
"foo": "bar", "foo": "bar",
"html": "<b>", "html": "<b>",
} }
@ -66,17 +41,17 @@ func TestRenderJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) 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() w := httptest.NewRecorder()
data := make(chan int) data := make(chan int)
// json: unsupported type: 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) { func TestRenderIndentedJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]any{
"foo": "bar", "foo": "bar",
"bar": "foo", "bar": "foo",
} }
@ -99,7 +74,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
func TestRenderSecureJSON(t *testing.T) { func TestRenderSecureJSON(t *testing.T) {
w1 := httptest.NewRecorder() w1 := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]any{
"foo": "bar", "foo": "bar",
} }
@ -113,7 +88,7 @@ func TestRenderSecureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder() w2 := httptest.NewRecorder()
datas := []map[string]interface{}{{ datas := []map[string]any{{
"foo": "bar", "foo": "bar",
}, { }, {
"bar": "foo", "bar": "foo",
@ -136,7 +111,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
func TestRenderJsonpJSON(t *testing.T) { func TestRenderJsonpJSON(t *testing.T) {
w1 := httptest.NewRecorder() w1 := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]any{
"foo": "bar", "foo": "bar",
} }
@ -146,11 +121,11 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1) err1 := (JsonpJSON{"x", data}).Render(w1)
assert.NoError(t, err1) assert.NoError(t, err1)
assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String()) assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder() w2 := httptest.NewRecorder()
datas := []map[string]interface{}{{ datas := []map[string]any{{
"foo": "bar", "foo": "bar",
}, { }, {
"bar": "foo", "bar": "foo",
@ -158,13 +133,58 @@ func TestRenderJsonpJSON(t *testing.T) {
err2 := (JsonpJSON{"x", datas}).Render(w2) err2 := (JsonpJSON{"x", datas}).Render(w2)
assert.NoError(t, err2) assert.NoError(t, err2)
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String()) assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type")) 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) { func TestRenderJsonpJSONError2(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]any{
"foo": "bar", "foo": "bar",
} }
(JsonpJSON{"", data}).WriteContentType(w) (JsonpJSON{"", data}).WriteContentType(w)
@ -188,7 +208,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
func TestRenderAsciiJSON(t *testing.T) { func TestRenderAsciiJSON(t *testing.T) {
w1 := httptest.NewRecorder() w1 := httptest.NewRecorder()
data1 := map[string]interface{}{ data1 := map[string]any{
"lang": "GO语言", "lang": "GO语言",
"tag": "<br>", "tag": "<br>",
} }
@ -200,7 +220,7 @@ func TestRenderAsciiJSON(t *testing.T) {
assert.Equal(t, "application/json", w1.Header().Get("Content-Type")) assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder() w2 := httptest.NewRecorder()
data2 := float64(3.1415926) data2 := 3.1415926
err = (AsciiJSON{data2}).Render(w2) err = (AsciiJSON{data2}).Render(w2)
assert.NoError(t, err) assert.NoError(t, err)
@ -217,7 +237,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
func TestRenderPureJSON(t *testing.T) { func TestRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]any{
"foo": "bar", "foo": "bar",
"html": "<b>", "html": "<b>",
} }
@ -227,7 +247,7 @@ func TestRenderPureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) 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 // Allows type H to be used with xml.Marshal
func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
@ -260,18 +280,18 @@ b:
d: [3, 4] d: [3, 4]
` `
(YAML{data}).WriteContentType(w) (YAML{data}).WriteContentType(w)
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w) err := (YAML{data}).Render(w)
assert.NoError(t, err) 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")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
type fail struct{} type fail struct{}
// Hook MarshalYAML // Hook MarshalYAML
func (ft *fail) MarshalYAML() (interface{}, error) { func (ft *fail) MarshalYAML() (any, error) {
return nil, errors.New("fail") return nil, errors.New("fail")
} }
@ -281,6 +301,27 @@ func TestRenderYAMLFail(t *testing.T) {
assert.Error(t, err) 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 // test Protobuf rendering
func TestRenderProtoBuf(t *testing.T) { func TestRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -347,7 +388,20 @@ func TestRenderRedirect(t *testing.T) {
} }
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) }) assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w)
assert.NoError(t, err)
})
data3 := Redirect{
Code: http.StatusCreated,
Request: req,
Location: "/new/location",
}
w = httptest.NewRecorder()
err = data3.Render(w)
assert.NoError(t, err)
// only improve coverage // only improve coverage
data2.WriteContentType(w) data2.WriteContentType(w)
@ -372,13 +426,13 @@ func TestRenderString(t *testing.T) {
(String{ (String{
Format: "hello %s %d", Format: "hello %s %d",
Data: []interface{}{}, Data: []any{},
}).WriteContentType(w) }).WriteContentType(w)
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
err := (String{ err := (String{
Format: "hola %s %d", Format: "hola %s %d",
Data: []interface{}{"manu", 2}, Data: []any{"manu", 2},
}).Render(w) }).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
@ -391,7 +445,7 @@ func TestRenderStringLenZero(t *testing.T) {
err := (String{ err := (String{
Format: "hola %s %d", Format: "hola %s %d",
Data: []interface{}{}, Data: []any{},
}).Render(w) }).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
@ -404,7 +458,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
htmlRender := HTMLProduction{Template: templ} htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("t", map[string]interface{}{ instance := htmlRender.Instance("t", map[string]any{
"name": "alexandernyquist", "name": "alexandernyquist",
}) })
@ -420,7 +474,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
templ := template.Must(template.New("").Parse(`Hello {{.name}}`)) templ := template.Must(template.New("").Parse(`Hello {{.name}}`))
htmlRender := HTMLProduction{Template: templ} htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("", map[string]interface{}{ instance := htmlRender.Instance("", map[string]any{
"name": "alexandernyquist", "name": "alexandernyquist",
}) })
@ -433,12 +487,13 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) { func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"}, htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "", Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"}, Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil, FuncMap: nil,
} }
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{ instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou", "name": "thinkerou",
}) })
@ -451,12 +506,13 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) { func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: nil, htmlRender := HTMLDebug{
Files: nil,
Glob: "../testdata/template/hello*", Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"}, Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil, FuncMap: nil,
} }
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{ instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou", "name": "thinkerou",
}) })
@ -468,7 +524,8 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
} }
func TestRenderHTMLDebugPanics(t *testing.T) { func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{Files: nil, htmlRender := HTMLDebug{
Files: nil,
Glob: "", Glob: "",
Delims: Delims{"{{", "}}"}, Delims: Delims{"{{", "}}"},
FuncMap: nil, FuncMap: nil,
@ -498,3 +555,39 @@ func TestRenderReader(t *testing.T) {
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
} }
func TestRenderReaderNoContentLength(t *testing.T) {
w := httptest.NewRecorder()
body := "#!PNG some raw data"
headers := make(map[string]string)
headers["Content-Disposition"] = `attachment; filename="filename.png"`
headers["x-request-id"] = "requestId"
err := (Reader{
ContentLength: -1,
ContentType: "image/png",
Reader: strings.NewReader(body),
Headers: headers,
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header())
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
}
func TestRenderWriteError(t *testing.T) {
data := []interface{}{"value1", "value2"}
prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{
bufString: prefix,
ResponseRecorder: httptest.NewRecorder(),
}
err := r.Render(ew)
assert.NotNil(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error())
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -6,14 +6,15 @@ package render
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// String contains the given interface object slice and its format. // String contains the given interface object slice and its format.
type String struct { type String struct {
Format string Format string
Data []interface{} Data []any
} }
var plainContentType = []string{"text/plain; charset=utf-8"} 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. // 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) writeContentType(w, plainContentType)
if len(data) > 0 { if len(data) > 0 {
_, err = fmt.Fprintf(w, format, data...) _, err = fmt.Fprintf(w, format, data...)
return return
} }
_, err = io.WriteString(w, format) _, err = w.Write(bytesconv.StringToBytes(format))
return 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

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -11,7 +11,7 @@ import (
// XML contains the given interface object. // XML contains the given interface object.
type XML struct { type XML struct {
Data interface{} Data any
} }
var xmlContentType = []string{"application/xml; charset=utf-8"} var xmlContentType = []string{"application/xml; charset=utf-8"}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,15 +7,15 @@ package render
import ( import (
"net/http" "net/http"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
// YAML contains the given interface object. // YAML contains the given interface object.
type YAML struct { type YAML struct {
Data interface{} Data any
} }
var yamlContentType = []string{"application/x-yaml; charset=utf-8"} var yamlContentType = []string{"application/yaml; charset=utf-8"}
// Render (YAML) marshals the given interface object and writes data with custom ContentType. // Render (YAML) marshals the given interface object and writes data with custom ContentType.
func (r YAML) Render(w http.ResponseWriter) error { func (r YAML) Render(w http.ResponseWriter) error {

Some files were not shown because too many files have changed in this diff Show More