Compare commits

..

No commits in common. "master" and "v1.7.0" have entirely different histories.

114 changed files with 4562 additions and 10615 deletions

View File

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

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 integration systems such as GitHub Actions. - It should pass all tests in the available continuous integration systems such as TravisCI.
- 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,10 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly

View File

@ -1,49 +0,0 @@
# 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

View File

@ -1,83 +0,0 @@
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: "^1"
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1.6
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ["1.23", "1.24"]
test-tags:
[
"",
"-tags nomsgpack",
'--ldflags="-checklinkname=0" -tags sonic',
"-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 }}

View File

@ -1,31 +0,0 @@
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@v6
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,7 +5,3 @@ count.out
test test
profile.out profile.out
tmp.out tmp.out
# Develop tools
.idea/
.vscode/

View File

@ -1,86 +0,0 @@
version: "2"
linters:
enable:
- asciicheck
- copyloopvar
- dogsled
- durationcheck
- errorlint
- gosec
- misspell
- nakedret
- nilerr
- nolintlint
- perfsprint
- revive
- testifylint
- usestdlibvars
- wastedassign
settings:
gosec:
includes:
- G102
- G106
- G108
- G109
- G111
- G112
- G201
- G203
perfsprint:
int-conversion: true
err-error: true
errorf: true
sprintf1: true
strconcat: true
testifylint:
enable-all: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- structcheck
- unused
text: '`data` is unused'
- linters:
- staticcheck
text: 'SA1019:'
- linters:
- revive
text: 'var-naming:'
- linters:
- revive
text: 'exported:'
- linters:
- gosec
path: _test\.go
- linters:
- revive
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- gin.go

View File

@ -1,56 +0,0 @@
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

50
.travis.yml Normal file
View File

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

View File

@ -2,405 +2,232 @@ 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 (@thinkerou), Javier Provecho (@javierprovecho) **Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@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.
- 178inaba <178inaba@users.noreply.github.com> **@858806258 (杰哥)**
- A. F <hello@clivern.com> - Fix typo in example
- ABHISHEK SONI <abhishek.rocks26@gmail.com>
- Abhishek Chanda <achanda@users.noreply.github.com>
- Abner Chen <houjunchen@gmail.com> **@achedeuzot (Klemen Sever)**
- AcoNCodes <acongame@gmail.com> - Fix newline debug printing
- Adam Dratwinski <adam.dratwinski@gmail.com>
- Adam Mckaig <adam.mckaig@gmail.com>
- Adam Zielinski <MusicAdam@users.noreply.github.com> **@adammck (Adam Mckaig)**
- Adonis <donileo@gmail.com> - Add MIT license
- Alan Wang <azzwacb9001@126.com>
- Albin Gilles <gilles.albin@gmail.com>
- Aleksandr Didenko <aa.didenko@yandex.ru> **@AlexanderChen1989 (Alexander)**
- Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> - Typos in README
- Alex <AWulkan@users.noreply.github.com>
- Alexander <alexanderchenmh@gmail.com>
- Alexander Lokhman <alex.lokhman@gmail.com> **@alexanderdidenko (Aleksandr Didenko)**
- Alexander Melentyev <55826637+alexander-melentyev@users.noreply.github.com> - Add support multipart/form-data
- Alexander Nyquist <nyquist.alexander@gmail.com>
- Allen Ren <kulong0105@gmail.com>
- AllinGo <tanhp@outlook.com> **@alexandernyquist (Alexander Nyquist)**
- Ammar Bandukwala <ammar@ammar.io> - Using template.Must to fix multiple return issue
- An Xiao (Luffy) <hac@zju.edu.cn> - ★ Added support for OPTIONS verb
- Andre Dublin <81dublin@gmail.com> - ★ Setting response headers before calling WriteHeader
- Andrew Szeto <github@jabagawee.com> - Improved documentation for model binding
- Andrey Abramov <andreyabramov.aaa@gmail.com> - ★ Added Content.Redirect()
- Andrey Nering <andrey.nering@gmail.com> - ★ Added tons of Unit tests
- Andrey Smirnov <Smirnov.Andrey@gmail.com>
- Andrii Bubis <firstrow@gmail.com>
- André Bazaglia <bazaglia@users.noreply.github.com> **@austinheap (Austin Heap)**
- Andy Pan <panjf2000@gmail.com> - Added travis CI integration
- Antoine GIRARD <sapk@users.noreply.github.com>
- Anup Kumar Panwar <1anuppanwar@gmail.com>
- Aravinth Sundaram <gosh.aravind@gmail.com> **@andredublin (Andre Dublin)**
- Artem <horechek@gmail.com> - Fix typo in comment
- Ashwani <ashwanisharma686@gmail.com>
- Aurelien Regat-Barrel <arb@cyberkarma.net>
- Austin Heap <me@austinheap.com> **@bredov (Ludwig Valda Vasquez)**
- Barnabus <jbampton@users.noreply.github.com> - Fix html templating in debug mode
- Bo-Yi Wu <appleboy.tw@gmail.com>
- Boris Borshevsky <BorisBorshevsky@gmail.com>
- Boyi Wu <p581581@gmail.com> **@bluele (Jun Kimura)**
- BradyBromley <51128276+BradyBromley@users.noreply.github.com> - Fixes code examples in README
- Brendan Fosberry <brendan@shopkeep.com>
- Brian Wigginton <brianwigginton@gmail.com>
- Carlos Eduardo <carlosedp@gmail.com> **@chad-russell**
- Chad Russell <chaddouglasrussell@gmail.com> - ★ Support for serializing gin.H into XML
- Charles <cxjava@gmail.com>
- Christian Muehlhaeuser <muesli@gmail.com>
- Christian Persson <saser@live.se> **@dickeyxxx (Jeff Dickey)**
- Christopher Harrington <ironiridis@gmail.com> - Typos in README
- Damon Zhao <yijun.zhao@outlook.com> - Add example about serving static files
- Dan Markham <dmarkham@gmail.com>
- Dang Nguyen <hoangdang.me@gmail.com>
- Daniel Krom <kromdan@gmail.com> **@donileo (Adonis)**
- Daniel M. Lambea <dmlambea@gmail.com> - Add NoMethod handler
- Danieliu <liudanking@gmail.com>
- David Irvine <aviddiviner@gmail.com>
- David Zhang <crispgm@gmail.com> **@dutchcoders (DutchCoders)**
- Davor Kapsa <dvrkps@users.noreply.github.com> - ★ Fix security bug that allows client to spoof ip
- DeathKing <DeathKing@users.noreply.github.com> - Fix typo. r.HTMLTemplates -> SetHTMLTemplate
- Dennis Cho <47404603+forest747@users.noreply.github.com>
- Dmitry Dorogin <dmirogin@ya.ru>
- Dmitry Kutakov <vkd.castle@gmail.com> **@el3ctro- (Joshua Loper)**
- Dmitry Sedykh <dmitrys@d3h.local> - Fix typo in example
- Don2Quixote <35610661+Don2Quixote@users.noreply.github.com>
- Donn Pebe <iam@donnpebe.com>
- Dustin Decker <dustindecker@protonmail.com> **@ethankan (Ethan Kan)**
- Eason Lin <easonlin404@gmail.com> - Unsigned integers in binding
- Edward Betts <edward@4angle.com>
- Egor Seredin <4819888+agmt@users.noreply.github.com>
- Emmanuel Goh <emmanuel@visenze.com> **(Evgeny Persienko)**
- Equim <sayaka@ekyu.moe> - Validate sub structures
- Eren A. Akyol <eren@redmc.me>
- Eric_Lee <xplzv@126.com>
- Erik Bender <erik.bender@develerik.dev> **@frankbille (Frank Bille)**
- Ethan Kan <ethankan@neoplot.com> - Add support for HTTP Realm Auth
- Evgeny Persienko <e.persienko@office.ngs.ru>
- Faisal Alam <ifaisalalam@gmail.com>
- Fareed Dudhia <fareeddudhia@googlemail.com> **@fmd (Fareed Dudhia)**
- Filip Figiel <figiel.filip@gmail.com> - Fix typo. SetHTTPTemplate -> SetHTMLTemplate
- Florian Polster <couchpolster@icqmail.com>
- Frank Bille <github@frankbille.dk>
- Franz Bettag <franz@bett.ag> **@ironiridis (Christopher Harrington)**
- Ganlv <ganlvtech@users.noreply.github.com> - Remove old reference
- Gaozhen Ying <yinggaozhen@hotmail.com>
- George Gabolaev <gabolaev98@gmail.com>
- George Kirilenko <necryin@users.noreply.github.com> **@jammie-stackhouse (Jamie Stackhouse)**
- Georges Varouchas <georges.varouchas@gmail.com> - Add more shortcuts for router methods
- Gordon Tyler <gordon@doxxx.net>
- Harindu Perera <harinduenator@gmail.com>
- Helios <674876158@qq.com> **@jasonrhansen**
- Henry Kwan <piengeng@users.noreply.github.com> - Fix spelling and grammar errors in documentation
- Henry Yee <henry@yearning.io>
- Himanshu Mishra <OrkoHunter@users.noreply.github.com>
- Hiroyuki Tanaka <h.tanaka.0325@gmail.com> **@JasonSoft (Jason Lee)**
- Ibraheem Ahmed <ibrah1440@gmail.com> - Fix typo in comment
- Ignacio Galindo <joiggama@gmail.com>
- Igor H. Vieira <zignd.igor@gmail.com>
- Ildar1111 <54001462+Ildar1111@users.noreply.github.com> **@joiggama (Ignacio Galindo)**
- Iskander (Alex) Sharipov <iskander.sharipov@intel.com> - Add utf-8 charset header on renders
- Ismail Gjevori <isgjevori@protonmail.com>
- Ivan Chen <allenivan@gmail.com>
- JINNOUCHI Yasushi <delphinus@remora.cx> **@julienschmidt (Julien Schmidt)**
- James Pettyjohn <japettyjohn@users.noreply.github.com> - gofmt the code examples
- Jamie Stackhouse <jamie.stackhouse@redspace.com>
- Jason Lee <jawc@hotmail.com>
- Javier Provecho <j.provecho@dartekstudios.com> **@kelcecil (Kel Cecil)**
- Javier Provecho <javier.provecho@bq.com> - Fix readme typo
- Javier Provecho <javiertitan@gmail.com>
- Javier Provecho Fernandez <j.provecho@dartekstudios.com>
- Javier Provecho Fernandez <javiertitan@gmail.com> **@kyledinh (Kyle Dinh)**
- Jean-Christophe Lebreton <jclebreton@gmail.com> - Adds RunTLS()
- Jeff <laojianzi1994@gmail.com>
- Jeremy Loy <jeremy.b.loy@icloud.com>
- Jim Filippou <p3160253@aueb.gr> **@LinusU (Linus Unnebäck)**
- Jimmy Pettersson <jimmy@expertmaker.com> - Small fixes in README
- John Bampton <jbampton@users.noreply.github.com>
- Johnny Dallas <johnnydallas0308@gmail.com>
- Johnny Dallas <theonlyjohnny@theonlyjohnny.sh> **@loongmxbt (Saint Asky)**
- Jonathan (JC) Chen <jc@dijonkitchen.org> - Fix typo in example
- Josep Jesus Bigorra Algaba <42377845+averageflow@users.noreply.github.com>
- Josh Horowitz <joshua.m.horowitz@gmail.com>
- Joshua Loper <josh.el3@gmail.com> **@lucas-clemente (Lucas Clemente)**
- Julien Schmidt <github@julienschmidt.com> - ★ work around path.Join removing trailing slashes from routes
- Jun Kimura <jksmphone@gmail.com>
- Justin Beckwith <justin.beckwith@gmail.com>
- Justin Israel <justinisrael@gmail.com> **@mattn (Yasuhiro Matsumoto)**
- Justin Mayhew <mayhew@live.ca> - Improve color logger
- Jérôme Laforge <jerome-laforge@users.noreply.github.com>
- Kacper Bąk <56700396+53jk1@users.noreply.github.com>
- Kamron Batman <kamronbatman@users.noreply.github.com> **@mdigger (Dmitry Sedykh)**
- Kane Rogers <kane@cleanstream.com.au> - Fixes Form binding when content-type is x-www-form-urlencoded
- Kaushik Neelichetty <kaushikneelichetty6132@gmail.com> - No repeat call c.Writer.Status() in gin.Logger
- Keiji Yoshida <yoshida.keiji.84@gmail.com> - Fixes Content-Type for json render
- Kel Cecil <kel.cecil@listhub.com>
- Kevin Mulvey <kmulvey@linux.com>
- Kevin Zhu <ipandtcp@gmail.com> **@mirzac (Mirza Ceric)**
- Kirill Motkov <motkov.kirill@gmail.com> - Fix debug printing
- Klemen Sever <ksever@student.42.fr>
- Kristoffer A. Iversen <kristoffer.a.iversen@gmail.com>
- Krzysztof Szafrański <k.p.szafranski@gmail.com> **@mopemope (Yutaka Matsubara)**
- Kumar McMillan <kumar.mcmillan@gmail.com> - ★ Adds Godep support (Dependencies Manager)
- Kyle Mcgill <email@kylescottmcgill.com> - Fix variadic parameter in the flexible render API
- Lanco <35420416+lancoLiu@users.noreply.github.com> - Fix Corrupted plain render
- Levi Olson <olson.levi@gmail.com> - Add Pluggable View Renderer Example
- Lin Kao-Yuan <mosdeo@gmail.com>
- Linus Unnebäck <linus@folkdatorn.se>
- Lucas Clemente <lucas@clemente.io> **@msemenistyi (Mykyta Semenistyi)**
- Ludwig Valda Vasquez <bredov@gmail.com> - update Readme.md. Add code to String method
- Luis GG <lggomez@users.noreply.github.com>
- MW Lim <williamchange@gmail.com>
- Maksimov Sergey <konjoot@gmail.com> **@msoedov (Sasha Myasoedov)**
- Manjusaka <lizheao940510@gmail.com> - ★ Adds tons of unit tests.
- Manu MA <manu.mtza@gmail.com>
- Manu MA <manu.valladolid@gmail.com>
- Manu Mtz-Almeida <manu.valladolid@gmail.com> **@ngerakines (Nick Gerakines)**
- Manu Mtz.-Almeida <manu.valladolid@gmail.com> - ★ Improves API, c.GET() doesn't panic
- Manuel Alonso <manuelalonso@invisionapp.com> - Adds MustGet() method
- Mara Kim <hacker.root@gmail.com>
- Mario Kostelac <mario@intercom.io>
- Martin Karlsch <martin@karlsch.com> **@r8k (Rajiv Kilaparti)**
- Matt Newberry <mnewberry@opentable.com> - Fix Port usage in README.
- Matt Williams <gh@mattyw.net>
- Matthieu MOREL <mmorel-35@users.noreply.github.com>
- Max Hilbrunner <mhilbrunner@users.noreply.github.com> **@rayrod2030 (Ray Rodriguez)**
- Maxime Soulé <btik-git@scoubidou.com> - Fix typo in example
- MetalBreaker <johnymichelson@gmail.com>
- Michael Puncel <mpuncel@squareup.com>
- MichaelDeSteven <51652084+MichaelDeSteven@users.noreply.github.com> **@rns**
- Mike <38686456+icy4ever@users.noreply.github.com> - Fix typo in example
- Mike Stipicevic <mst@ableton.com>
- Miki Tebeka <miki.tebeka@gmail.com>
- Miles <MilesLin@users.noreply.github.com> **@RobAWilkinson (Robert Wilkinson)**
- Mirza Ceric <mirza.ceric@b2match.com> - Add example of forms and params
- Mykyta Semenistyi <nikeiwe@gmail.com>
- Naoki Takano <honten@tinkermode.com>
- Ngalim Siregar <ngalim.siregar@gmail.com> **@rogierlommers (Rogier Lommers)**
- Ni Hao <supernihaooo@qq.com> - Add updated static serve example
- Nick Gerakines <nick@gerakines.net>
- Nikifor Seryakov <nikandfor@gmail.com> **@rw-access (Ross Wolf)**
- Notealot <714804968@qq.com> - Added support to mix exact and param routes
- Olivier Mengué <dolmen@cpan.org>
- Olivier Robardet <orobardet@users.noreply.github.com> **@se77en (Damon Zhao)**
- Pablo Moncada <pablo.moncada@bq.com> - Improve color logging
- Pablo Moncada <pmoncadaisla@gmail.com>
- Panmax <967168@qq.com>
- Peperoncino <2wua4nlyi@gmail.com> **@silasb (Silas Baronda)**
- Philipp Meinen <philipp@bind.ch> - Fixing quotes in README
- Pierre Massat <pierre@massat.io>
- Qt <golang.chen@gmail.com>
- Quentin ROYER <aydendevg@gmail.com> **@SkuliOskarsson (Skuli Oskarsson)**
- README Bot <35302948+codetriage-readme-bot@users.noreply.github.com> - Fixes some texts in README II
- Rafal Zajac <rzajac@gmail.com>
- Rahul Datta Roy <rahuldroy@users.noreply.github.com>
- Rajiv Kilaparti <rajivk085@gmail.com> **@slimmy (Jimmy Pettersson)**
- Raphael Gavache <raphael.gavache@datadoghq.com> - Added messages for required bindings
- Ray Rodriguez <rayrod2030@gmail.com>
- Regner Blok-Andersen <shadowdf@gmail.com>
- Remco <remco@dutchcoders.io> **@smira (Andrey Smirnov)**
- Rex Lee(李俊) <duguying2008@gmail.com> - Add support for ignored/unexported fields in binding
- Richard Lee <dlackty@gmail.com>
- Riverside <wangyb65@gmail.com>
- Robert Wilkinson <wilkinson.robert.a@gmail.com> **@superalsrk (SRK.Lyu)**
- Rogier Lommers <rogier@lommers.org> - Update httprouter godeps
- Rohan Pai <me@rohanpai.com>
- Romain Beuque <rbeuque74@gmail.com>
- Roman Belyakovsky <ihryamzik@gmail.com> **@tebeka (Miki Tebeka)**
- Roman Zaynetdinov <627197+zaynetro@users.noreply.github.com> - Use net/http constants instead of numeric values
- Roman Zaynetdinov <roman.zaynetdinov@lekane.com>
- Ronald Petty <ronald.petty@rx-m.com>
- Ross Wolf <31489089+rw-access@users.noreply.github.com> **@techjanitor**
- Roy Lou <roylou@gmail.com> - Update context.go reserved IPs
- Rubi <14269809+codenoid@users.noreply.github.com>
- Ryan <46182144+ryanker@users.noreply.github.com>
- Ryan J. Yoder <me@ryanjyoder.com> **@yosssi (Keiji Yoshida)**
- SRK.Lyu <superalsrk@gmail.com> - Fix link in README
- Sai <sairoutine@gmail.com>
- Samuel Abreu <sdepaula@gmail.com>
- Santhosh Kumar <santhoshkumarr1096@gmail.com> **@yuyabee**
- Sasha Melentyev <sasha@melentyev.io> - Fixed README
- 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>

View File

@ -1,246 +1,8 @@
# Gin ChangeLog # Gin ChangeLog
## 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 ## Gin v1.7.0
### BUG FIXES ### BUGFIXES
* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600)) * 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: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
@ -255,7 +17,7 @@
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378)) * 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)) * 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)) * 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)) * remove a unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389)) * Add 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)) * 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)) * binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
@ -279,44 +41,33 @@
## Gin v1.6.2 ## Gin v1.6.2
### BUG FIXES ### BUGFIXES
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305) * fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
### ENHANCEMENTS ### ENHANCEMENTS
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306) * Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
## Gin v1.6.1 ## Gin v1.6.1
### BUG FIXES ### BUGFIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294) * Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0 ## Gin v1.6.0
### BREAKING ### BREAKING
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159) * 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) * 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) * Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES ### FEATURES
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220) * add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112) * FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### BUGFIXES
### BUG FIXES
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280) * 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) * 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) * 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) * 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) * [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) * Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
### ENHANCEMENTS ### ENHANCEMENTS
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277) * 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: 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) * tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
@ -331,9 +82,7 @@
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149) * 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) * 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) * Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
### DOCS ### DOCS
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223) * 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) * 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) * Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
@ -346,9 +95,7 @@
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165) * 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) * 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) * Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
### MISC ### MISC
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262) * 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) * 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) * Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
@ -488,7 +235,7 @@
- [FIX] Refactor render - [FIX] Refactor render
- [FIX] Reworked tests - [FIX] Reworked tests
- [FIX] logger now supports cygwin - [FIX] logger now supports cygwin
- [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

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 integration systems such as GitHub Actions. - It should pass all tests in the available continuous integration systems such as TravisCI.
- 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,6 +1,5 @@
GO ?= go GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
PACKAGES ?= $(shell $(GO) list ./...) PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/) VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go") GOFILES := $(shell find . -name "*.go")
@ -8,11 +7,10 @@ TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | gr
TESTTAGS ?= "" TESTTAGS ?= ""
.PHONY: test .PHONY: test
# Run tests to verify code functionality.
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
$(GO) test $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \ $(GO) test -tags $(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; \
@ -31,12 +29,10 @@ test:
done done
.PHONY: fmt .PHONY: fmt
# Ensure consistent code formatting.
fmt: fmt:
$(GOFMT) -w $(GOFILES) $(GOFMT) -w $(GOFILES)
.PHONY: fmt-check .PHONY: fmt-check
# format (check only).
fmt-check: fmt-check:
@diff=$$($(GOFMT) -d $(GOFILES)); \ @diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
@ -45,62 +41,31 @@ fmt-check:
exit 1; \ exit 1; \
fi; fi;
.PHONY: vet
# Examine packages and report suspicious constructs if any.
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
.PHONY: lint .PHONY: lint
# Inspect source code for stylistic errors or potential bugs.
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u golang.org/x/lint/golint; \ $(GO) get -u golang.org/x/lint/golint; \
fi fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell
# Correct commonly misspelled English words in source code.
misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -w $(GOFILES)
.PHONY: misspell-check .PHONY: misspell-check
# misspell (check only).
misspell-check: misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -error $(GOFILES) misspell -error $(GOFILES)
.PHONY: tools .PHONY: misspell
# Install tools (golint and misspell). misspell:
tools: @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@if [ $(GO_VERSION) -gt 15 ]; then \ $(GO) get -u 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 fi
misspell -w $(GOFILES)
.PHONY: help .PHONY: tools
# Help. tools:
help: go install golang.org/x/lint/golint; \
@echo '' go install github.com/client9/misspell/cmd/misspell;
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

2222
README.md

File diff suppressed because it is too large Load Diff

29
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.
@ -16,9 +16,6 @@ import (
// 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
@ -34,7 +31,7 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false return "", false
} }
for _, pair := range a { for _, pair := range a {
if subtle.ConstantTimeCompare(bytesconv.StringToBytes(pair.value), bytesconv.StringToBytes(authValue)) == 1 { if subtle.ConstantTimeCompare([]byte(pair.value), []byte(authValue)) == 1 {
return pair.user, true return pair.user, true
} }
} }
@ -92,25 +89,3 @@ func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
} }
// BasicAuthForProxy returns a Basic HTTP Proxy-Authorization middleware.
// If the realm is empty, "Proxy Authorization Required" will be used by default.
func BasicAuthForProxy(accounts Accounts, realm string) HandlerFunc {
if realm == "" {
realm = "Proxy Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
pairs := processAccounts(accounts)
return func(c *Context) {
proxyUser, found := pairs.searchCredential(c.requestHeader("Proxy-Authorization"))
if !found {
// Credentials doesn't match, we return 407 and abort handlers chain.
c.Header("Proxy-Authenticate", realm)
c.AbortWithStatus(http.StatusProxyAuthRequired)
return
}
// The proxy_user credentials was found, set proxy_user's id to key AuthProxyUserKey in this context, the proxy_user's id can be read later using
// c.MustGet(gin.AuthProxyUserKey).
c.Set(AuthProxyUserKey, proxyUser)
}
}

View File

@ -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.
@ -90,7 +90,7 @@ func TestBasicAuthSucceed(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/login", nil) req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", authorizationHeader("admin", "password")) req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -109,7 +109,7 @@ func TestBasicAuth401(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/login", nil) req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -129,7 +129,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/login", nil) req, _ := http.NewRequest("GET", "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -137,40 +137,3 @@ 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(http.MethodGet, "/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(http.MethodGet, "/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.
@ -14,21 +14,21 @@ import (
func BenchmarkOneRoute(B *testing.B) { func BenchmarkOneRoute(B *testing.B) {
router := New() router := New()
router.GET("/ping", func(c *Context) {}) router.GET("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func BenchmarkRecoveryMiddleware(B *testing.B) { func BenchmarkRecoveryMiddleware(B *testing.B) {
router := New() router := New()
router.Use(Recovery()) router.Use(Recovery())
router.GET("/", func(c *Context) {}) router.GET("/", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/") runRequest(B, router, "GET", "/")
} }
func BenchmarkLoggerMiddleware(B *testing.B) { func BenchmarkLoggerMiddleware(B *testing.B) {
router := New() router := New()
router.Use(LoggerWithWriter(newMockWriter())) router.Use(LoggerWithWriter(newMockWriter()))
router.GET("/", func(c *Context) {}) router.GET("/", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/") runRequest(B, router, "GET", "/")
} }
func BenchmarkManyHandlers(B *testing.B) { func BenchmarkManyHandlers(B *testing.B) {
@ -37,7 +37,7 @@ func BenchmarkManyHandlers(B *testing.B) {
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.GET("/ping", func(c *Context) {}) router.GET("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func Benchmark5Params(B *testing.B) { func Benchmark5Params(B *testing.B) {
@ -45,7 +45,7 @@ func Benchmark5Params(B *testing.B) {
router := New() router := New()
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {}) router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/param/path/to/parameter/john/12345") runRequest(B, router, "GET", "/param/path/to/parameter/john/12345")
} }
func BenchmarkOneRouteJSON(B *testing.B) { func BenchmarkOneRouteJSON(B *testing.B) {
@ -56,7 +56,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
router.GET("/json", func(c *Context) { router.GET("/json", func(c *Context) {
c.JSON(http.StatusOK, data) c.JSON(http.StatusOK, data)
}) })
runRequest(B, router, http.MethodGet, "/json") runRequest(B, router, "GET", "/json")
} }
func BenchmarkOneRouteHTML(B *testing.B) { func BenchmarkOneRouteHTML(B *testing.B) {
@ -68,7 +68,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.GET("/html", func(c *Context) { router.GET("/html", func(c *Context) {
c.HTML(http.StatusOK, "index", "hola") c.HTML(http.StatusOK, "index", "hola")
}) })
runRequest(B, router, http.MethodGet, "/html") runRequest(B, router, "GET", "/html")
} }
func BenchmarkOneRouteSet(B *testing.B) { func BenchmarkOneRouteSet(B *testing.B) {
@ -76,7 +76,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
router.GET("/ping", func(c *Context) { router.GET("/ping", func(c *Context) {
c.Set("key", "value") c.Set("key", "value")
}) })
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func BenchmarkOneRouteString(B *testing.B) { func BenchmarkOneRouteString(B *testing.B) {
@ -84,13 +84,13 @@ func BenchmarkOneRouteString(B *testing.B) {
router.GET("/text", func(c *Context) { router.GET("/text", func(c *Context) {
c.String(http.StatusOK, "this is a plain text") c.String(http.StatusOK, "this is a plain text")
}) })
runRequest(B, router, http.MethodGet, "/text") runRequest(B, router, "GET", "/text")
} }
func BenchmarkManyRoutesFist(B *testing.B) { func BenchmarkManyRoutesFist(B *testing.B) {
router := New() router := New()
router.Any("/ping", func(c *Context) {}) router.Any("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func BenchmarkManyRoutesLast(B *testing.B) { func BenchmarkManyRoutesLast(B *testing.B) {
@ -103,7 +103,7 @@ func Benchmark404(B *testing.B) {
router := New() router := New()
router.Any("/something", func(c *Context) {}) router.Any("/something", func(c *Context) {})
router.NoRoute(func(c *Context) {}) router.NoRoute(func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping") runRequest(B, router, "GET", "/ping")
} }
func Benchmark404Many(B *testing.B) { func Benchmark404Many(B *testing.B) {
@ -118,7 +118,7 @@ func Benchmark404Many(B *testing.B) {
router.GET("/user/:id/:mode", func(c *Context) {}) router.GET("/user/:id/:mode", func(c *Context) {})
router.NoRoute(func(c *Context) {}) router.NoRoute(func(c *Context) {})
runRequest(B, router, http.MethodGet, "/viewfake") runRequest(B, router, "GET", "/viewfake")
} }
type mockWriter struct { type mockWriter struct {

View File

@ -1,8 +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 //go:build !nomsgpack
// +build !nomsgpack
package binding package binding
@ -21,8 +22,6 @@ 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
@ -30,27 +29,27 @@ const (
// the form POST. // the form POST.
type Binding interface { type Binding interface {
Name() string Name() string
Bind(*http.Request, any) error Bind(*http.Request, interface{}) 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, any) error BindBody([]byte, interface{}) 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 reads the Params. // but it read the Params.
type BindingUri interface { type BindingUri interface {
Name() string Name() string
BindUri(map[string][]string, any) error BindUri(map[string][]string, interface{}) 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/v10.6.1. // https://github.com/go-playground/validator/tree/v8.18.2.
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 a slice|array, the validation should be performed travel on every element. // If the received type is a slice|array, the validation should be performed travel on every element.
@ -58,34 +57,32 @@ type StructValidator interface {
// 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(any) error ValidateStruct(interface{}) error
// Engine returns the underlying validator engine which powers the // Engine returns the underlying validator engine which powers the
// StructValidator implementation. // StructValidator implementation.
Engine() any Engine() interface{}
} }
// 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/v10.6.1 // interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// 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 BindingBody = jsonBinding{} JSON = jsonBinding{}
XML BindingBody = xmlBinding{} XML = xmlBinding{}
Form Binding = formBinding{} Form = formBinding{}
Query Binding = queryBinding{} Query = queryBinding{}
FormPost Binding = formPostBinding{} FormPost = formPostBinding{}
FormMultipart Binding = formMultipartBinding{} FormMultipart = formMultipartBinding{}
ProtoBuf BindingBody = protobufBinding{} ProtoBuf = protobufBinding{}
MsgPack BindingBody = msgpackBinding{} MsgPack = msgpackBinding{}
YAML BindingBody = yamlBinding{} YAML = yamlBinding{}
Uri BindingUri = uriBinding{} Uri = uriBinding{}
Header Binding = headerBinding{} Header = headerBinding{}
Plain BindingBody = plainBinding{}
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
@ -104,10 +101,8 @@ func Default(method, contentType string) Binding {
return ProtoBuf return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2: case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack return MsgPack
case MIMEYAML, MIMEYAML2: case MIMEYAML:
return YAML return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart return FormMultipart
default: // case MIMEPOSTForm: default: // case MIMEPOSTForm:
@ -115,7 +110,7 @@ func Default(method, contentType string) Binding {
} }
} }
func validate(obj any) error { func validate(obj interface{}) error {
if Validator == nil { if Validator == nil {
return nil return nil
} }

View File

@ -3,16 +3,15 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack //go:build !nomsgpack
// +build !nomsgpack
package binding package binding
import ( import (
"bytes" "bytes"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -26,7 +25,7 @@ func TestBindingMsgPack(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf) assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test) err := codec.NewEncoder(buf, h).Encode(test)
require.NoError(t, err) assert.NoError(t, err)
data := buf.Bytes() data := buf.Bytes()
@ -40,20 +39,20 @@ func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body,
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
obj := FooStruct{} obj := FooStruct{}
req := requestWithBody(http.MethodPost, path, body) req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK) req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj) err := b.Bind(req, &obj)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{} obj = FooStruct{}
req = requestWithBody(http.MethodPost, badPath, badBody) req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK) req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj) err = MsgPack.Bind(req, &obj)
require.Error(t, err) assert.Error(t, err)
} }
func TestBindingDefaultMsgPack(t *testing.T) { func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK)) assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2)) assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
} }

View File

@ -3,6 +3,7 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build nomsgpack //go:build nomsgpack
// +build nomsgpack
package binding package binding
@ -19,8 +20,6 @@ const (
MIMEMultipartPOSTForm = "multipart/form-data" MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf" MIMEPROTOBUF = "application/x-protobuf"
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
@ -28,42 +27,42 @@ const (
// the form POST. // the form POST.
type Binding interface { type Binding interface {
Name() string Name() string
Bind(*http.Request, any) error Bind(*http.Request, interface{}) 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, any) error BindBody([]byte, interface{}) 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 reads the Params. // but it read the Params.
type BindingUri interface { type BindingUri interface {
Name() string Name() string
BindUri(map[string][]string, any) error BindUri(map[string][]string, interface{}) 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/v10.6.1. // https://github.com/go-playground/validator/tree/v8.18.2.
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 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 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(any) error ValidateStruct(interface{}) error
// Engine returns the underlying validator engine which powers the // Engine returns the underlying validator engine which powers the
// StructValidator implementation. // StructValidator implementation.
Engine() any Engine() interface{}
} }
// 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/v10.6.1 // interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// under the hood. // under the hood.
var Validator StructValidator = &defaultValidator{} var Validator StructValidator = &defaultValidator{}
@ -80,8 +79,6 @@ var (
YAML = yamlBinding{} YAML = yamlBinding{}
Uri = uriBinding{} Uri = uriBinding{}
Header = headerBinding{} Header = headerBinding{}
TOML = tomlBinding{}
Plain = plainBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -98,18 +95,16 @@ func Default(method, contentType string) Binding {
return XML return XML
case MIMEPROTOBUF: case MIMEPROTOBUF:
return ProtoBuf return ProtoBuf
case MIMEYAML, MIMEYAML2: case MIMEYAML:
return YAML return YAML
case MIMEMultipartPOSTForm: case MIMEMultipartPOSTForm:
return FormMultipart return FormMultipart
case MIMETOML:
return TOML
default: // case MIMEPOSTForm: default: // case MIMEPOSTForm:
return Form return Form
} }
} }
func validate(obj any) error { func validate(obj interface{}) error {
if Validator == nil { if Validator == nil {
return nil return nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
// 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"
"strconv"
"strings" "strings"
"sync" "sync"
@ -18,30 +18,23 @@ type defaultValidator struct {
validate *validator.Validate validate *validator.Validate
} }
type SliceValidationError []error type sliceValidateError []error
// Error concatenates all error elements in SliceValidationError into a single string separated by \n. func (err sliceValidateError) Error() string {
func (err SliceValidationError) Error() string { var errMsgs []string
if len(err) == 0 { for i, e := range err {
return "" if e == nil {
} continue
var b strings.Builder
for i := 0; i < len(err); i++ {
if err[i] != nil {
if b.Len() > 0 {
b.WriteString("\n")
}
b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
} }
errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
} }
return b.String() return strings.Join(errMsgs, "\n")
} }
var _ StructValidator = (*defaultValidator)(nil) var _ StructValidator = &defaultValidator{}
// 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 any) error { func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if obj == nil { if obj == nil {
return nil return nil
} }
@ -49,15 +42,12 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
value := reflect.ValueOf(obj) value := reflect.ValueOf(obj)
switch value.Kind() { switch value.Kind() {
case reflect.Ptr: case reflect.Ptr:
if value.Elem().Kind() != reflect.Struct { return v.ValidateStruct(value.Elem().Interface())
return v.ValidateStruct(value.Elem().Interface())
}
return v.validateStruct(obj)
case reflect.Struct: case reflect.Struct:
return v.validateStruct(obj) return v.validateStruct(obj)
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
count := value.Len() count := value.Len()
validateRet := make(SliceValidationError, 0) validateRet := make(sliceValidateError, 0)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err) validateRet = append(validateRet, err)
@ -73,7 +63,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
} }
// validateStruct receives struct type // validateStruct receives struct type
func (v *defaultValidator) validateStruct(obj any) error { func (v *defaultValidator) validateStruct(obj interface{}) error {
v.lazyinit() v.lazyinit()
return v.validate.Struct(obj) return v.validate.Struct(obj)
} }
@ -81,8 +71,8 @@ func (v *defaultValidator) validateStruct(obj any) error {
// 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://pkg.go.dev/github.com/go-playground/validator/v10 // https://godoc.org/gopkg.in/go-playground/validator.v8
func (v *defaultValidator) Engine() any { func (v *defaultValidator) Engine() interface{} {
v.lazyinit() v.lazyinit()
return v.validate return v.validate
} }

View File

@ -1,28 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"strconv"
"testing"
)
func BenchmarkSliceValidationError(b *testing.B) {
const size int = 100
e := make(SliceValidationError, size)
for j := 0; j < size; j++ {
e[j] = errors.New(strconv.Itoa(j))
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if len(e.Error()) == 0 {
b.Errorf("error")
}
}
}

View File

@ -9,40 +9,18 @@ import (
"testing" "testing"
) )
func TestSliceValidationError(t *testing.T) { func TestSliceValidateError(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
err SliceValidationError err sliceValidateError
want string want string
}{ }{
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"}, {"has nil elements", sliceValidateError{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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want { if got := tt.err.Error(); got != tt.want {
t.Errorf("SliceValidationError.Error() = %v, want %v", got, tt.want) t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
} }
}) })
} }
@ -56,7 +34,7 @@ func TestDefaultValidator(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
v *defaultValidator v *defaultValidator
obj any obj interface{}
wantErr bool wantErr bool
}{ }{
{"validate nil obj", &defaultValidator{}, nil, false}, {"validate nil obj", &defaultValidator{}, nil, false},

View File

@ -1,32 +1,31 @@
// 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 (
"errors"
"net/http" "net/http"
) )
const defaultMemory = 32 << 20 const defaultMemory = 32 << 20
type ( type formBinding struct{}
formBinding struct{} type formPostBinding struct{}
formPostBinding struct{} type formMultipartBinding struct{}
formMultipartBinding struct{}
)
func (formBinding) Name() string { func (formBinding) Name() string {
return "form" return "form"
} }
func (formBinding) Bind(req *http.Request, obj any) error { func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) { if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err if err != http.ErrNotMultipart {
return err
}
} }
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err
@ -38,7 +37,7 @@ func (formPostBinding) Name() string {
return "form-urlencoded" return "form-urlencoded"
} }
func (formPostBinding) Bind(req *http.Request, obj any) error { func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
@ -52,7 +51,7 @@ func (formMultipartBinding) Name() string {
return "multipart/form-data" return "multipart/form-data"
} }
func (formMultipartBinding) Bind(req *http.Request, obj any) error { func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err return 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.
@ -7,44 +7,31 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
) )
var ( var errUnknownType = errors.New("unknown type")
errUnknownType = errors.New("unknown type")
// ErrConvertMapStringSlice can not convert to map[string][]string func mapUri(ptr interface{}, m map[string][]string) error {
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 any, form map[string][]string) error { func mapForm(ptr interface{}, 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 any, form map[string][]string, tag string) error { func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
// Check if ptr is a map // Check if ptr is a map
ptrVal := reflect.ValueOf(ptr) ptrVal := reflect.ValueOf(ptr)
var pointed any var pointed interface{}
if ptrVal.Kind() == reflect.Ptr { if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem() ptrVal = ptrVal.Elem()
pointed = ptrVal.Interface() pointed = ptrVal.Interface()
@ -62,7 +49,7 @@ func mapFormByTag(ptr any, form map[string][]string, tag string) error {
// 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) (isSet bool, err error) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
} }
type formSource map[string][]string type formSource map[string][]string
@ -70,11 +57,11 @@ 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) (isSet bool, err error) { func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setByForm(value, field, form, tagValue, opt) return setByForm(value, field, form, tagValue, opt)
} }
func mappingByPtr(ptr any, setter setter, tag string) error { func mappingByPtr(ptr interface{}, 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
} }
@ -84,7 +71,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
return false, nil return false, nil
} }
vKind := value.Kind() var vKind = value.Kind()
if vKind == reflect.Ptr { if vKind == reflect.Ptr {
var isNew bool var isNew bool
@ -93,14 +80,14 @@ 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())
} }
isSet, err := mapping(vPtr.Elem(), field, setter, tag) isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil { if err != nil {
return false, err return false, err
} }
if isNew && isSet { if isNew && isSetted {
value.Set(vPtr) value.Set(vPtr)
} }
return isSet, nil return isSetted, nil
} }
if vKind != reflect.Struct || !field.Anonymous { if vKind != reflect.Struct || !field.Anonymous {
@ -116,19 +103,19 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
if vKind == reflect.Struct { if vKind == reflect.Struct {
tValue := value.Type() tValue := value.Type()
var isSet bool var isSetted bool
for i := 0; i < value.NumField(); i++ { for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i) sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue continue
} }
ok, err := mapping(value.Field(i), sf, setter, tag) ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
if err != nil { if err != nil {
return false, err return false, err
} }
isSet = isSet || ok isSetted = isSetted || ok
} }
return isSet, nil return isSetted, nil
} }
return false, nil return false, nil
} }
@ -159,70 +146,13 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
if k, v := head(opt, "="); k == "default" { if k, v := head(opt, "="); k == "default" {
setOpt.isDefaultExists = true setOpt.isDefaultExists = true
setOpt.defaultValue = v setOpt.defaultValue = v
// convert semicolon-separated default values to csv-separated values for processing in setByForm
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
} }
} }
return setter.TrySet(value, field, tagValue, setOpt) return setter.TrySet(value, field, tagValue, setOpt)
} }
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method. func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
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 trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
return vs, nil
}
var sep string
switch cfTag {
case "csv":
sep = ","
case "ssv":
sep = " "
case "tsv":
sep = "\t"
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
}
totalLength := 0
for _, v := range vs {
totalLength += strings.Count(v, sep) + 1
}
newVs = make([]string, 0, totalLength)
for _, v := range vs {
newVs = append(newVs, strings.Split(v, sep)...)
}
return newVs, 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
@ -232,46 +162,15 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
case reflect.Slice: case reflect.Slice:
if !ok { if !ok {
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
}
} }
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
return true, setSlice(vs, value, field) return true, setSlice(vs, value, field)
case reflect.Array: case reflect.Array:
if !ok { if !ok {
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
}
} }
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
if len(vs) != value.Len() { if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
} }
return true, setArray(vs, value, field) return true, setArray(vs, value, field)
default: default:
var val string var val string
@ -281,12 +180,6 @@ 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 val == "" {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
return ok, err
} }
return true, setWithProperType(val, value, field) return true, setWithProperType(val, value, field)
} }
@ -305,7 +198,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) return setTimeDuration(val, value, field)
} }
return setIntField(val, 64, value) return setIntField(val, 64, value)
case reflect.Uint: case reflect.Uint:
@ -330,17 +223,10 @@ 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.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.API.Unmarshal(bytesconv.StringToBytes(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
} }
@ -398,26 +284,21 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
} }
switch tf := strings.ToLower(timeFormat); tf { switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixmilli", "unixmicro", "unixnano": case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64) tv, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return err return err
} }
var t time.Time d := time.Duration(1)
switch tf { if tf == "unixnano" {
case "unix": d = time.Second
t = time.Unix(tv, 0)
case "unixmilli":
t = time.UnixMilli(tv)
case "unixmicro":
t = time.UnixMicro(tv)
default:
t = time.Unix(0, tv)
} }
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
if val == "" { if val == "" {
@ -467,7 +348,7 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
return nil return nil
} }
func setTimeDuration(val string, value reflect.Value) error { func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
d, err := time.ParseDuration(val) d, err := time.ParseDuration(val)
if err != nil { if err != nil {
return err return err
@ -477,17 +358,20 @@ func setTimeDuration(val string, value reflect.Value) error {
} }
func head(str, sep string) (head string, tail string) { func head(str, sep string) (head string, tail string) {
head, tail, _ = strings.Cut(str, sep) idx := strings.Index(str, sep)
return head, tail if idx < 0 {
return str, ""
}
return str[:idx], str[idx+len(sep):]
} }
func setFormMap(ptr any, form map[string][]string) error { func setFormMap(ptr interface{}, form map[string][]string) error {
el := reflect.TypeOf(ptr).Elem() el := reflect.TypeOf(ptr).Elem()
if el.Kind() == reflect.Slice { if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string) ptrMap, ok := ptr.(map[string][]string)
if !ok { if !ok {
return ErrConvertMapStringSlice return errors.New("cannot convert to map slices of strings")
} }
for k, v := range form { for k, v := range form {
ptrMap[k] = v ptrMap[k] = v
@ -498,7 +382,7 @@ func setFormMap(ptr any, form map[string][]string) error {
ptrMap, ok := ptr.(map[string]string) ptrMap, ok := ptr.(map[string]string)
if !ok { if !ok {
return ErrConvertToMapString return errors.New("cannot convert to map of strings")
} }
for k, v := range form { for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last ptrMap[k] = v[len(v)-1] // pick last

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.

View File

@ -5,17 +5,11 @@
package binding package binding
import ( import (
"encoding/hex"
"errors"
"mime/multipart"
"reflect" "reflect"
"strconv"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMappingBaseTypes(t *testing.T) { func TestMappingBaseTypes(t *testing.T) {
@ -24,9 +18,9 @@ func TestMappingBaseTypes(t *testing.T) {
} }
for _, tt := range []struct { for _, tt := range []struct {
name string name string
value any value interface{}
form string form string
expect any expect interface{}
}{ }{
{"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)},
@ -49,7 +43,6 @@ 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()
@ -60,7 +53,7 @@ func TestMappingBaseTypes(t *testing.T) {
field := val.Elem().Type().Field(0) field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form") _, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
require.NoError(t, err, testName) assert.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface() actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName) assert.Equal(t, tt.expect, actual, testName)
@ -69,15 +62,13 @@ func TestMappingBaseTypes(t *testing.T) {
func TestMappingDefault(t *testing.T) { func TestMappingDefault(t *testing.T) {
var s struct { var s struct {
Str string `form:",default=defaultVal"`
Int int `form:",default=9"` Int int `form:",default=9"`
Slice []int `form:",default=9"` Slice []int `form:",default=9"`
Array [1]int `form:",default=9"` Array [1]int `form:",default=9"`
} }
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "defaultVal", s.Str)
assert.Equal(t, 9, s.Int) assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice) assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array) assert.Equal(t, [1]int{9}, s.Array)
@ -88,7 +79,7 @@ func TestMappingSkipField(t *testing.T) {
A int A int
} }
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, s.A) assert.Equal(t, 0, s.A)
} }
@ -99,7 +90,7 @@ func TestMappingIgnoreField(t *testing.T) {
B int `form:"-"` B int `form:"-"`
} }
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form") err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 9, s.A) assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B) assert.Equal(t, 0, s.B)
@ -111,7 +102,7 @@ func TestMappingUnexportedField(t *testing.T) {
b int `form:"b"` b int `form:"b"`
} }
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form") err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 9, s.A) assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b) assert.Equal(t, 0, s.b)
@ -122,8 +113,8 @@ func TestMappingPrivateField(t *testing.T) {
f int `form:"field"` f int `form:"field"`
} }
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form") err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, s.f) assert.Equal(t, int(0), s.f)
} }
func TestMappingUnknownFieldType(t *testing.T) { func TestMappingUnknownFieldType(t *testing.T) {
@ -132,7 +123,7 @@ func TestMappingUnknownFieldType(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form") err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
require.Error(t, err) assert.Error(t, err)
assert.Equal(t, errUnknownType, err) assert.Equal(t, errUnknownType, err)
} }
@ -140,9 +131,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"}})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, int(6), s.F)
} }
func TestMappingForm(t *testing.T) { func TestMappingForm(t *testing.T) {
@ -150,35 +141,8 @@ func TestMappingForm(t *testing.T) {
F int `form:"field"` F int `form:"field"`
} }
err := mapForm(&s, map[string][]string{"field": {"6"}}) err := mapForm(&s, map[string][]string{"field": {"6"}})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, int(6), s.F)
}
func TestMappingFormFieldNotSent(t *testing.T) {
var s struct {
F string `form:"field,default=defVal"`
}
err := mapForm(&s, map[string][]string{})
require.NoError(t, err)
assert.Equal(t, "defVal", s.F)
}
func TestMappingFormWithEmptyToDefault(t *testing.T) {
var s struct {
F string `form:"field,default=DefVal"`
}
err := mapForm(&s, map[string][]string{"field": {""}})
require.NoError(t, err)
assert.Equal(t, "DefVal", s.F)
}
func TestMapFormWithTag(t *testing.T) {
var s struct {
F int `externalTag:"field"`
}
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
require.NoError(t, err)
assert.Equal(t, 6, s.F)
} }
func TestMappingTime(t *testing.T) { func TestMappingTime(t *testing.T) {
@ -192,7 +156,7 @@ func TestMappingTime(t *testing.T) {
var err error var err error
time.Local, err = time.LoadLocation("Europe/Berlin") time.Local, err = time.LoadLocation("Europe/Berlin")
require.NoError(t, err) assert.NoError(t, err)
err = mapForm(&s, map[string][]string{ err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"}, "Time": {"2019-01-20T16:02:58Z"},
@ -201,7 +165,7 @@ func TestMappingTime(t *testing.T) {
"CSTTime": {"2019-01-20"}, "CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"}, "UTCTime": {"2019-01-20"},
}) })
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String()) assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String()) assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
@ -216,14 +180,14 @@ func TestMappingTime(t *testing.T) {
Time time.Time `time_location:"wrong"` Time time.Time `time_location:"wrong"`
} }
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}}) err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
require.Error(t, err) assert.Error(t, err)
// wrong time value // wrong time value
var wrongTime struct { var wrongTime struct {
Time time.Time Time time.Time
} }
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}}) err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
require.Error(t, err) assert.Error(t, err)
} }
func TestMappingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
@ -233,12 +197,12 @@ func TestMappingTimeDuration(t *testing.T) {
// ok // ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form") err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D) assert.Equal(t, 5*time.Second, s.D)
// error // error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err) assert.Error(t, err)
} }
func TestMappingSlice(t *testing.T) { func TestMappingSlice(t *testing.T) {
@ -248,17 +212,17 @@ func TestMappingSlice(t *testing.T) {
// default value // default value
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice) assert.Equal(t, []int{9}, s.Slice)
// ok // ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form") err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice) assert.Equal(t, []int{3, 4}, s.Slice)
// error // error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
require.Error(t, err) assert.Error(t, err)
} }
func TestMappingArray(t *testing.T) { func TestMappingArray(t *testing.T) {
@ -268,125 +232,20 @@ func TestMappingArray(t *testing.T) {
// wrong default // wrong default
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.Error(t, err) assert.Error(t, err)
// ok // ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form") err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array) assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals // error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form") err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
require.Error(t, err) assert.Error(t, err)
// error - wrong value // error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
require.Error(t, err) assert.Error(t, err)
}
func TestMappingCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
SliceTsv []int `form:"slice_tsv" collection_format:"tsv"`
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
ArrayTsv [2]int `form:"array_tsv" collection_format:"tsv"`
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{
"slice_multi": {"1", "2"},
"slice_csv": {"1,2"},
"slice_ssv": {"1 2"},
"slice_tsv": {"1 2"},
"slice_pipes": {"1|2"},
"array_multi": {"1", "2"},
"array_csv": {"1,2"},
"array_ssv": {"1 2"},
"array_tsv": {"1 2"},
"array_pipes": {"1|2"},
}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2}, s.SliceMulti)
assert.Equal(t, []int{1, 2}, s.SliceCsv)
assert.Equal(t, []int{1, 2}, s.SliceSsv)
assert.Equal(t, []int{1, 2}, s.SliceTsv)
assert.Equal(t, []int{1, 2}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
}
func TestMappingCollectionFormatInvalid(t *testing.T) {
var s struct {
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
}
err := mappingByPtr(&s, formSource{
"slice_csv": {"1,2"},
}, "form")
require.Error(t, err)
var s2 struct {
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
}
err = mappingByPtr(&s2, formSource{
"array_csv": {"1,2"},
}, "form")
require.Error(t, err)
}
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
assert.Equal(t, []int{1, 2, 3}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
} }
func TestMappingStructField(t *testing.T) { func TestMappingStructField(t *testing.T) {
@ -397,50 +256,17 @@ func TestMappingStructField(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form") err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
require.NoError(t, err) assert.NoError(t, err)
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")
require.NoError(t, err)
assert.Empty(t, req0.Items)
// With 1 item.
var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
require.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")
require.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
} }
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form") err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
require.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)
} }
@ -451,187 +277,5 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
var s S var s S
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err) 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")
require.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")
require.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 errors.New("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")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(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")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(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")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(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")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
type customPath []string
func (p *customPath) UnmarshalParam(param string) error {
elems := strings.Split(param, "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
func TestMappingCustomSliceUri(t *testing.T) {
var s struct {
FileData customPath `uri:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceForm(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error {
oid, err := convertTo(param)
if err != nil {
return err
}
*o = oid
return nil
}
func convertTo(s string) (objectID, error) {
var nilObjectID objectID
if len(s) != 24 {
return nilObjectID, errors.New("invalid format")
}
var oid [12]byte
_, err := hex.Decode(oid[:], []byte(s))
if err != nil {
return nilObjectID, err
}
return oid, nil
}
func TestMappingCustomArrayUri(t *testing.T) {
var s struct {
FileData objectID `uri:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayForm(t *testing.T) {
var s struct {
FileData objectID `form:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.Equal(t, expected, s.FileData)
} }

View File

@ -1,7 +1,3 @@
// 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 package binding
import ( import (
@ -16,7 +12,8 @@ func (headerBinding) Name() string {
return "header" return "header"
} }
func (headerBinding) Bind(req *http.Request, obj any) error { func (headerBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapHeader(obj, req.Header); err != nil { if err := mapHeader(obj, req.Header); err != nil {
return err return err
} }
@ -24,7 +21,7 @@ func (headerBinding) Bind(req *http.Request, obj any) error {
return validate(obj) return validate(obj)
} }
func mapHeader(ptr any, h map[string][]string) error { func mapHeader(ptr interface{}, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header") return mappingByPtr(ptr, headerSource(h), "header")
} }
@ -32,6 +29,6 @@ type headerSource map[string][]string
var _ setter = headerSource(nil) var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (bool, error) { func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt) 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,16 +6,16 @@ package binding
import ( import (
"bytes" "bytes"
"errors" "fmt"
"io" "io"
"net/http" "net/http"
"github.com/gin-gonic/gin/codec/json" "github.com/gin-gonic/gin/internal/json"
) )
// 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
// any as a Number instead of as a float64. // interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false var EnableDecoderUseNumber = false
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method // EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
@ -30,19 +30,19 @@ func (jsonBinding) Name() string {
return "json" return "json"
} }
func (jsonBinding) Bind(req *http.Request, obj any) error { func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil { if req == nil || req.Body == nil {
return errors.New("invalid request") return fmt.Errorf("invalid request")
} }
return decodeJSON(req.Body, obj) return decodeJSON(req.Body, obj)
} }
func (jsonBinding) BindBody(body []byte, obj any) error { func (jsonBinding) BindBody(body []byte, obj interface{}) error {
return decodeJSON(bytes.NewReader(body), obj) return decodeJSON(bytes.NewReader(body), obj)
} }
func decodeJSON(r io.Reader, obj any) error { func decodeJSON(r io.Reader, obj interface{}) error {
decoder := json.API.NewDecoder(r) decoder := json.NewDecoder(r)
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }

View File

@ -5,16 +5,8 @@
package binding package binding
import ( import (
"io"
"net/http/httptest"
"testing" "testing"
"time"
"unsafe"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/render"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -36,181 +28,3 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
assert.Equal(t, "FOO", s["foo"]) assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"]) assert.Equal(t, "world", s["hello"])
} }
func TestCustomJsonCodec(t *testing.T) {
// Restore json encoding configuration after testing
oldMarshal := json.API
defer func() {
json.API = oldMarshal
}()
// Custom json api
json.API = customJsonApi{}
// test decode json
obj := customReq{}
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
require.NoError(t, err)
assert.Equal(t, zeroTime, obj.TimeEmpty)
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
assert.Nil(t, obj.TimeNil)
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
// test encode json
w := httptest.NewRecorder()
err2 := (render.PureJSON{Data: obj}).Render(w)
require.NoError(t, err2)
assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
type customReq struct {
TimeEmpty time.Time `json:"time_empty"`
TimeStruct time.Time `json:"time_struct"`
TimeNil *time.Time `json:"time_nil"`
TimePointer *time.Time `json:"time_pointer"`
}
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
func init() {
customConfig.RegisterExtension(&TimeEx{})
customConfig.RegisterExtension(&TimePointerEx{})
}
type customJsonApi struct{}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
// region Time Extension
var (
zeroTime = time.Time{}
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
defaultTimeCodec = &timeCodec{}
)
type TimeEx struct {
jsoniter.DummyExtension
}
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
type timeCodec struct{}
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((*time.Time)(ptr))
return t.Equal(zeroTime)
}
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((*time.Time)(ptr))
if t.Equal(zeroTime) {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((*time.Time)(ptr)) = zeroTime
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((*time.Time)(ptr)) = t
}
// endregion
// region *Time Extension
var (
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
defaultTimePointerCodec = &timePointerCodec{}
)
type TimePointerEx struct {
jsoniter.DummyExtension
}
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
type timePointerCodec struct{}
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((**time.Time)(ptr))
return t == nil || (*t).Equal(zeroTime)
}
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((**time.Time)(ptr))
if t == nil || (*t).Equal(zeroTime) {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((**time.Time)(ptr)) = nil
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((**time.Time)(ptr)) = &t
}
// endregion

View File

@ -1,8 +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 //go:build !nomsgpack
// +build !nomsgpack
package binding package binding
@ -20,15 +21,15 @@ func (msgpackBinding) Name() string {
return "msgpack" return "msgpack"
} }
func (msgpackBinding) Bind(req *http.Request, obj any) error { func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
return decodeMsgPack(req.Body, obj) return decodeMsgPack(req.Body, obj)
} }
func (msgpackBinding) BindBody(body []byte, obj any) error { func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
return decodeMsgPack(bytes.NewReader(body), obj) return decodeMsgPack(bytes.NewReader(body), obj)
} }
func decodeMsgPack(r io.Reader, obj any) error { func decodeMsgPack(r io.Reader, obj interface{}) 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

View File

@ -3,6 +3,7 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !nomsgpack //go:build !nomsgpack
// +build !nomsgpack
package binding package binding
@ -25,7 +26,7 @@ func TestMsgpackBindingBindBody(t *testing.T) {
assert.Equal(t, "FOO", s.Foo) assert.Equal(t, "FOO", s.Foo)
} }
func msgpackBody(t *testing.T, obj any) []byte { func msgpackBody(t *testing.T, obj interface{}) []byte {
var bs bytes.Buffer var bs bytes.Buffer
h := &codec.MsgpackHandle{} h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj) err := codec.NewEncoder(&bs, h).Encode(obj)

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.
@ -15,16 +15,8 @@ type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil) 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 // 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) { func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if files := r.MultipartForm.File[key]; len(files) != 0 { if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files) return setByMultipartFormFile(value, field, files)
} }
@ -32,7 +24,7 @@ func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField
return setByForm(value, field, r.MultipartForm.Value, key, opt) return setByForm(value, field, r.MultipartForm.Value, key, opt)
} }
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) { func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
switch value.Kind() { switch value.Kind() {
case reflect.Ptr: case reflect.Ptr:
switch value.Interface().(type) { switch value.Interface().(type) {
@ -48,26 +40,26 @@ func setByMultipartFormFile(value reflect.Value, field reflect.StructField, file
} }
case reflect.Slice: case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files)) slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSet, err = setArrayOfMultipartFormFiles(slice, field, files) isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSet { if err != nil || !isSetted {
return isSet, err return isSetted, err
} }
value.Set(slice) value.Set(slice)
return true, nil return true, nil
case reflect.Array: case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files) return setArrayOfMultipartFormFiles(value, field, files)
} }
return false, ErrMultiFileHeader return false, errors.New("unsupported field type for multipart.FileHeader")
} }
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSet bool, err error) { func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
if value.Len() != len(files) { if value.Len() != len(files) {
return false, ErrMultiFileHeaderLenInvalid return false, errors.New("unsupported len of array for []*multipart.FileHeader")
} }
for i := range files { for i := range files {
set, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1]) setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !set { if err != nil || !setted {
return set, err return setted, err
} }
} }
return true, nil return true, 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.
@ -6,13 +6,12 @@ package binding
import ( import (
"bytes" "bytes"
"io" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestFormMultipartBindingBindOneFile(t *testing.T) { func TestFormMultipartBindingBindOneFile(t *testing.T) {
@ -28,7 +27,7 @@ func TestFormMultipartBindingBindOneFile(t *testing.T) {
req := createRequestMultipartFiles(t, file) req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s) err := FormMultipart.Bind(req, &s)
require.NoError(t, err) assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file) assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file) assertMultipartFileHeader(t, s.FilePtr, file)
@ -54,7 +53,7 @@ func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
req := createRequestMultipartFiles(t, files...) req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s) err := FormMultipart.Bind(req, &s)
require.NoError(t, err) assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files)) assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files)) assert.Len(t, s.SlicePtrs, len(files))
@ -77,7 +76,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
s any s interface{}
}{ }{
{"wrong type", &struct { {"wrong type", &struct {
Files int `form:"file"` Files int `form:"file"`
@ -91,7 +90,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
} { } {
req := createRequestMultipartFiles(t, files...) req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s) err := FormMultipart.Bind(req, tt.s)
require.Error(t, err) assert.Error(t, err)
} }
} }
@ -107,17 +106,17 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
mw := multipart.NewWriter(&body) mw := multipart.NewWriter(&body)
for _, file := range files { for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename) fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
require.NoError(t, err) assert.NoError(t, err)
n, err := fw.Write(file.Content) n, err := fw.Write(file.Content)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, len(file.Content), n) assert.Equal(t, len(file.Content), n)
} }
err := mw.Close() err := mw.Close()
require.NoError(t, err) assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, "/", &body) req, err := http.NewRequest("POST", "/", &body)
require.NoError(t, err) assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary()) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req return req
@ -125,15 +124,15 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) { func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename) assert.Equal(t, file.Filename, fh.Filename)
assert.Equal(t, int64(len(file.Content)), fh.Size) // assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
fl, err := fh.Open() fl, err := fh.Open()
require.NoError(t, err) assert.NoError(t, err)
body, err := io.ReadAll(fl) body, err := ioutil.ReadAll(fl)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body)) assert.Equal(t, string(file.Content), string(body))
err = fl.Close() err = fl.Close()
require.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -1,56 +0,0 @@
package binding
import (
"fmt"
"io"
"net/http"
"reflect"
"github.com/gin-gonic/gin/internal/bytesconv"
)
type plainBinding struct{}
func (plainBinding) Name() string {
return "plain"
}
func (plainBinding) Bind(req *http.Request, obj any) error {
all, err := io.ReadAll(req.Body)
if err != nil {
return err
}
return decodePlain(all, obj)
}
func (plainBinding) BindBody(body []byte, obj any) error {
return decodePlain(body, obj)
}
func decodePlain(data []byte, obj any) error {
if obj == nil {
return nil
}
v := reflect.ValueOf(obj)
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() == reflect.String {
v.SetString(bytesconv.BytesToString(data))
return nil
}
if _, ok := v.Interface().([]byte); ok {
v.SetBytes(data)
return nil
}
return fmt.Errorf("type (%T) unknown type", v)
}

View File

@ -1,15 +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 binding package binding
import ( import (
"errors" "io/ioutil"
"io"
"net/http" "net/http"
"google.golang.org/protobuf/proto" "github.com/golang/protobuf/proto"
) )
type protobufBinding struct{} type protobufBinding struct{}
@ -18,23 +17,19 @@ func (protobufBinding) Name() string {
return "protobuf" return "protobuf"
} }
func (b protobufBinding) Bind(req *http.Request, obj any) error { func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
buf, err := io.ReadAll(req.Body) buf, err := ioutil.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 any) error { func (protobufBinding) BindBody(body []byte, obj interface{}) error {
msg, ok := obj.(proto.Message) if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
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 until now we can't add // Here it's same to return validate(obj), but util now we can't add
// `binding:""` to the struct which automatically generate by gen-proto // `binding:""` to the struct which automatically generate by gen-proto
return nil return nil
// return validate(obj) // return validate(obj)

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 any) error { func (queryBinding) Bind(req *http.Request, obj interface{}) 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

View File

@ -1,35 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
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 validate(obj)
}

View File

@ -1,22 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
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 any) error { func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
if err := mapURI(obj, m); err != nil { if err := mapUri(obj, m); err != nil {
return err return 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.
@ -11,7 +11,6 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type testInterface interface { type testInterface interface {
@ -60,7 +59,7 @@ type structNoValidationValues struct {
StructSlice []substructNoValidation StructSlice []substructNoValidation
InterfaceSlice []testInterface InterfaceSlice []testInterface
UniversalInterface any UniversalInterface interface{}
CustomInterface testInterface CustomInterface testInterface
FloatMap map[string]float32 FloatMap map[string]float32
@ -114,10 +113,10 @@ func TestValidateNoValidationValues(t *testing.T) {
test := createNoValidationValues() test := createNoValidationValues()
empty := structNoValidationValues{} empty := structNoValidationValues{}
require.NoError(t, validate(test)) assert.Nil(t, validate(test))
require.NoError(t, validate(&test)) assert.Nil(t, validate(&test))
require.NoError(t, validate(empty)) assert.Nil(t, validate(empty))
require.NoError(t, validate(&empty)) assert.Nil(t, validate(&empty))
assert.Equal(t, origin, test) assert.Equal(t, origin, test)
} }
@ -158,65 +157,41 @@ type structNoValidationPointer struct {
} }
func TestValidateNoValidationPointers(t *testing.T) { func TestValidateNoValidationPointers(t *testing.T) {
// origin := createNoValidation_values() //origin := createNoValidation_values()
// test := createNoValidation_values() //test := createNoValidation_values()
empty := structNoValidationPointer{} empty := structNoValidationPointer{}
// assert.Nil(t, validate(test)) //assert.Nil(t, validate(test))
// assert.Nil(t, validate(&test)) //assert.Nil(t, validate(&test))
require.NoError(t, validate(empty)) assert.Nil(t, validate(empty))
require.NoError(t, validate(&empty)) assert.Nil(t, validate(&empty))
// assert.Equal(t, origin, test) //assert.Equal(t, origin, test)
} }
type Object map[string]any type Object map[string]interface{}
func TestValidatePrimitives(t *testing.T) { func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1} obj := Object{"foo": "bar", "bar": 1}
require.NoError(t, validate(obj)) assert.NoError(t, validate(obj))
require.NoError(t, validate(&obj)) assert.NoError(t, validate(&obj))
assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj) assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}} obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
require.NoError(t, validate(obj2)) assert.NoError(t, validate(obj2))
require.NoError(t, validate(&obj2)) assert.NoError(t, validate(&obj2))
nu := 10 nu := 10
require.NoError(t, validate(nu)) assert.NoError(t, validate(nu))
require.NoError(t, validate(&nu)) assert.NoError(t, validate(&nu))
assert.Equal(t, 10, nu) assert.Equal(t, 10, nu)
str := "value" str := "value"
require.NoError(t, validate(str)) assert.NoError(t, validate(str))
require.NoError(t, validate(&str)) assert.NoError(t, validate(&str))
assert.Equal(t, "value", str) assert.Equal(t, "value", str)
} }
type structModifyValidation struct {
Integer int
}
func toZero(sl validator.StructLevel) {
s := 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)
require.NoError(t, errs)
assert.Equal(t, structModifyValidation{Integer: 0}, s)
}
// 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.
@ -240,14 +215,14 @@ func TestValidatorEngine(t *testing.T) {
err := engine.RegisterValidation("notone", notOne) err := engine.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error // Check that we can register custom validation without error
require.NoError(t, err) assert.Nil(t, err)
// Create an instance which will fail validation // Create an instance which will fail validation
withOne := structCustomValidation{Integer: 1} withOne := structCustomValidation{Integer: 1}
errs := validate(withOne) errs := validate(withOne)
// Check that we got back non-nil errs // Check that we got back non-nil errs
require.Error(t, errs) assert.NotNil(t, errs)
// Check that the error matches expectation // Check that the error matches expectation
require.Error(t, errs, "notone") assert.Error(t, errs, "", "", "notone")
} }

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,15 +17,14 @@ func (xmlBinding) Name() string {
return "xml" return "xml"
} }
func (xmlBinding) Bind(req *http.Request, obj any) error { func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
return decodeXML(req.Body, obj) return decodeXML(req.Body, obj)
} }
func (xmlBinding) BindBody(body []byte, obj any) error { func (xmlBinding) BindBody(body []byte, obj interface{}) 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

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"
"github.com/goccy/go-yaml" "gopkg.in/yaml.v2"
) )
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 any) error { func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
return decodeYAML(req.Body, obj) return decodeYAML(req.Body, obj)
} }
func (yamlBinding) BindBody(body []byte, obj any) error { func (yamlBinding) BindBody(body []byte, obj interface{}) error {
return decodeYAML(bytes.NewReader(body), obj) return decodeYAML(bytes.NewReader(body), obj)
} }
func decodeYAML(r io.Reader, obj any) error { func decodeYAML(r io.Reader, obj interface{}) 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

View File

@ -1,57 +0,0 @@
// Copyright 2025 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 json
import "io"
// API the json codec in use.
var API Core
// Core the api for json codec.
type Core interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
MarshalIndent(v any, prefix, indent string) ([]byte, error)
NewEncoder(writer io.Writer) Encoder
NewDecoder(reader io.Reader) Decoder
}
// Encoder an interface writes JSON values to an output stream.
type Encoder interface {
// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
// to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability
// of the output, SetEscapeHTML(false) disables this behavior.
SetEscapeHTML(on bool)
// Encode writes the JSON encoding of v to the stream,
// followed by a newline character.
//
// See the documentation for Marshal for details about the
// conversion of Go values to JSON.
Encode(v any) error
}
// Decoder an interface reads and decodes JSON values from an input stream.
type Decoder interface {
// UseNumber causes the Decoder to unmarshal a number into an any as a
// Number instead of as a float64.
UseNumber()
// 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.
DisallowUnknownFields()
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
Decode(v any) error
}

View File

@ -1,42 +0,0 @@
// Copyright 2025 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 go_json
package json
import (
"io"
"github.com/goccy/go-json"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/goccy/go-json"
func init() {
API = gojsonApi{}
}
type gojsonApi struct{}
func (j gojsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j gojsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -1,41 +0,0 @@
// Copyright 2025 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 !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
package json
import (
"encoding/json"
"io"
)
// Package indicates what library is being used for JSON encoding.
const Package = "encoding/json"
func init() {
API = jsonApi{}
}
type jsonApi struct{}
func (j jsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -1,44 +0,0 @@
// Copyright 2025 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 jsoniter
package json
import (
"io"
jsoniter "github.com/json-iterator/go"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/json-iterator/go"
func init() {
API = jsoniterApi{}
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type jsoniterApi struct{}
func (j jsoniterApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsoniterApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsoniterApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsoniterApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsoniterApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -1,44 +0,0 @@
// Copyright 2025 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 && (linux || windows || darwin)
package json
import (
"io"
"github.com/bytedance/sonic"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/bytedance/sonic"
func init() {
API = sonicApi{}
}
var json = sonic.ConfigStd
type sonicApi struct{}
func (j sonicApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j sonicApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j sonicApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j sonicApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j sonicApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,12 @@
// 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 //go:build appengine
// +build appengine
package gin package gin
func init() { func init() {
defaultPlatform = PlatformGoogleAppEngine defaultAppEngine = true
} }

File diff suppressed because it is too large Load Diff

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,23 +10,19 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
) )
const ginSupportMinGoVer = 21 const ginSupportMinGoVer = 12
// 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.
func IsDebugging() bool { func IsDebugging() bool {
return atomic.LoadInt32(&ginMode) == debugCode return ginMode == debugCode
} }
// 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 ...any)
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
@ -51,20 +47,13 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
} }
} }
func debugPrint(format string, values ...any) { func debugPrint(format string, values ...interface{}) {
if !IsDebugging() { if IsDebugging() {
return if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[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) {
@ -77,8 +66,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.23+. debugPrint(`[WARNING] Now Gin requires Go 1.12+.
`) `)
} }
@ -106,7 +95,9 @@ at initialization. ie. before any route is registered or the router is listening
} }
func debugPrintError(err error) { func debugPrintError(err error) {
if err != nil && IsDebugging() { if err != nil {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err) if IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
} }
} }

View File

@ -1,29 +1,27 @@
// 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"
"html/template" "html/template"
"io" "io"
"log" "log"
"net/http"
"os" "os"
"runtime" "runtime"
"strings"
"sync" "sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// TODO // TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { // func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...any) { // func debugPrint(format string, values ...interface{}) {
func TestIsDebugging(t *testing.T) { func TestIsDebugging(t *testing.T) {
SetMode(DebugMode) SetMode(DebugMode)
@ -61,7 +59,7 @@ func TestDebugPrintError(t *testing.T) {
func TestDebugPrintRoutes(t *testing.T) { func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
}) })
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)
@ -73,7 +71,7 @@ func TestDebugPrintRouteFunc(t *testing.T) {
} }
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
}) })
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
@ -105,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.23+.\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.12+.\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)
} }
@ -140,7 +138,7 @@ func captureOutput(t *testing.T, f func()) string {
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)
wg.Add(1) wg.Add(1)
go func() { go func() {
var buf strings.Builder var buf bytes.Buffer
wg.Done() wg.Done()
_, err := io.Copy(&buf, reader) _, err := io.Copy(&buf, reader)
assert.NoError(t, err) assert.NoError(t, err)
@ -156,13 +154,13 @@ func TestGetMinVer(t *testing.T) {
var m uint64 var m uint64
var e error var e error
_, e = getMinVer("go1") _, e = getMinVer("go1")
require.Error(t, e) assert.NotNil(t, e)
m, e = getMinVer("go1.1") m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m) assert.Equal(t, uint64(1), m)
require.NoError(t, e) assert.Nil(t, e)
m, e = getMinVer("go1.1.1") m, e = getMinVer("go1.1.1")
require.NoError(t, e) assert.Nil(t, e)
assert.Equal(t, uint64(1), m) assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1") _, e = getMinVer("go1.1.1.1")
require.Error(t, e) assert.NotNil(t, e)
} }

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,10 +12,8 @@ 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 {
// Deprecated: Use MustBindWith or ShouldBindWith. log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to
func (c *Context) BindWith(obj any, b binding.Binding) error {
log.Println(`BindWith(\"any, binding.Binding\") error is going to
be deprecated, please check issue #662 and either use MustBindWith() if you 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.
@ -18,7 +18,7 @@ func TestBindWith(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`

File diff suppressed because it is too large Load Diff

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,7 +9,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/gin-gonic/gin/codec/json" "github.com/gin-gonic/gin/internal/json"
) )
// ErrorType is an unsigned 64-bit error code as defined in the gin spec. // ErrorType is an unsigned 64-bit error code as defined in the gin spec.
@ -34,12 +34,12 @@ const (
type Error struct { type Error struct {
Err error Err error
Type ErrorType Type ErrorType
Meta any Meta interface{}
} }
type errorMsgs []*Error type errorMsgs []*Error
var _ error = (*Error)(nil) var _ error = &Error{}
// 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,13 +48,13 @@ 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 any) *Error { func (msg *Error) SetMeta(data interface{}) *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() any { func (msg *Error) JSON() interface{} {
jsonData := H{} jsonData := H{}
if msg.Meta != nil { if msg.Meta != nil {
value := reflect.ValueOf(msg.Meta) value := reflect.ValueOf(msg.Meta)
@ -77,7 +77,7 @@ func (msg *Error) JSON() any {
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (msg *Error) MarshalJSON() ([]byte, error) { func (msg *Error) MarshalJSON() ([]byte, error) {
return json.API.Marshal(msg.JSON()) return json.Marshal(msg.JSON())
} }
// Error implements the error interface. // Error implements the error interface.
@ -91,7 +91,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
} }
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap() // Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg Error) Unwrap() error { func (msg *Error) Unwrap() error {
return msg.Err return msg.Err
} }
@ -122,13 +122,12 @@ func (a errorMsgs) Last() *Error {
return nil return nil
} }
// Errors returns an array with all the error messages. // Errors returns an array will all the error messages.
// Example: // Example:
// // c.Error(errors.New("first"))
// c.Error(errors.New("first")) // c.Error(errors.New("second"))
// c.Error(errors.New("second")) // c.Error(errors.New("third"))
// c.Error(errors.New("third")) // c.Errors.Errors() // == []string{"first", "second", "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
@ -140,14 +139,14 @@ func (a errorMsgs) Errors() []string {
return errorStrings return errorStrings
} }
func (a errorMsgs) JSON() any { func (a errorMsgs) JSON() interface{} {
switch length := len(a); length { 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:
jsonData := make([]any, length) jsonData := make([]interface{}, length)
for i, err := range a { for i, err := range a {
jsonData[i] = err.JSON() jsonData[i] = err.JSON()
} }
@ -157,7 +156,7 @@ func (a errorMsgs) JSON() any {
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) { func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.API.Marshal(a.JSON()) return json.Marshal(a.JSON())
} }
func (a errorMsgs) String() string { func (a errorMsgs) String() string {

34
errors_1.13_test.go Normal file
View File

@ -0,0 +1,34 @@
//go:build go1.13
// +build go1.13
package gin
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
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,
// hence the "// +build go1.13" directive at the beginning of this file.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("somme 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))
}

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,10 @@ package gin
import ( import (
"errors" "errors"
"fmt"
"testing" "testing"
"github.com/gin-gonic/gin/codec/json" "github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestError(t *testing.T) { func TestError(t *testing.T) {
@ -33,10 +31,10 @@ func TestError(t *testing.T) {
"meta": "some data", "meta": "some data",
}, err.JSON()) }, err.JSON())
jsonBytes, _ := json.API.Marshal(err) jsonBytes, _ := json.Marshal(err)
assert.JSONEq(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",
}) })
@ -46,7 +44,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",
@ -61,7 +59,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())
} }
@ -87,54 +85,22 @@ Error #02: second
Error #03: third Error #03: third
Meta: map[status:400] Meta: map[status:400]
`, errs.String()) `, errs.String())
assert.Equal(t, []any{ assert.Equal(t, []interface{}{
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"},
}, errs.JSON()) }, errs.JSON())
jsonBytes, _ := json.API.Marshal(errs) jsonBytes, _ := json.Marshal(errs)
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes)) assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
errs = errorMsgs{ errs = errorMsgs{
{Err: errors.New("first"), Type: ErrorTypePrivate}, {Err: errors.New("first"), Type: ErrorTypePrivate},
} }
assert.Equal(t, H{"error": "first"}, errs.JSON()) assert.Equal(t, H{"error": "first"}, errs.JSON())
jsonBytes, _ = json.API.Marshal(errs) jsonBytes, _ = json.Marshal(errs)
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
errs = errorMsgs{} errs = errorMsgs{}
assert.Nil(t, errs.Last()) assert.Nil(t, errs.Last())
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 :
require.ErrorIs(t, err, innerErr)
var testErr TestErr
require.ErrorAs(t, err, &testErr)
// Test non-pointer usage of gin.Error
errNonPointer := Error{
Err: innerErr,
Type: ErrorTypeAny,
}
wrappedErr := fmt.Errorf("wrapped: %w", errNonPointer)
// Check that 'errors.Is()' and 'errors.As()' behave as expected for non-pointer usage
require.ErrorIs(t, wrappedErr, innerErr)
var testErrNonPointer TestErr
require.ErrorAs(t, wrappedErr, &testErrNonPointer)
}

53
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,42 +9,37 @@ import (
"os" "os"
) )
// OnlyFilesFS implements an http.FileSystem without `Readdir` functionality. type onlyFilesFS struct {
type OnlyFilesFS struct { fs http.FileSystem
FileSystem http.FileSystem
} }
// Open passes `Open` to the upstream implementation without `Readdir` functionality. type neuteredReaddirFile struct {
func (o OnlyFilesFS) Open(name string) (http.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
return neutralizedReaddirFile{f}, nil
}
// neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`.
type neutralizedReaddirFile struct {
http.File http.File
} }
// Readdir overrides the http.File default implementation and always returns nil. // Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
func (n neutralizedReaddirFile) Readdir(_ int) ([]os.FileInfo, error) { // in router.Static().
// this disables directory listing // if listDirectory == true, then it works the same as http.Dir() otherwise it returns
return nil, nil // a filesystem that prevents http.FileServer() to list the directory files.
}
// Dir returns an http.FileSystem that can be used by http.FileServer().
// It is used internally in router.Static().
// if listDirectory == true, then it works the same as http.Dir(),
// otherwise it returns a filesystem that prevents http.FileServer() to list the directory files.
func Dir(root string, listDirectory bool) http.FileSystem { func Dir(root string, listDirectory bool) http.FileSystem {
fs := http.Dir(root) fs := http.Dir(root)
if listDirectory { if listDirectory {
return fs return fs
} }
return &onlyFilesFS{fs}
return &OnlyFilesFS{FileSystem: fs} }
// Open conforms to http.Filesystem.
func (fs onlyFilesFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
return neuteredReaddirFile{f}, nil
}
// Readdir overrides the http.File default implementation.
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
// this disables directory listing
return nil, nil
} }

View File

@ -1,72 +0,0 @@
package gin
import (
"errors"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
open func(name string) (http.File, error)
}
func (m *mockFileSystem) Open(name string) (http.File, error) {
return m.open(name)
}
func TestOnlyFilesFS_Open(t *testing.T) {
var testFile *os.File
mockFS := &mockFileSystem{
open: func(name string) (http.File, error) {
return testFile, nil
},
}
fs := &OnlyFilesFS{FileSystem: mockFS}
file, err := fs.Open("foo")
require.NoError(t, err)
assert.Equal(t, testFile, file.(neutralizedReaddirFile).File)
}
func TestOnlyFilesFS_Open_err(t *testing.T) {
testError := errors.New("mock")
mockFS := &mockFileSystem{
open: func(_ string) (http.File, error) {
return nil, testError
},
}
fs := &OnlyFilesFS{FileSystem: mockFS}
file, err := fs.Open("foo")
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}
func Test_neuteredReaddirFile_Readdir(t *testing.T) {
n := neutralizedReaddirFile{}
res, err := n.Readdir(0)
require.NoError(t, err)
assert.Nil(t, res)
}
func TestDir_listDirectory(t *testing.T) {
testRoot := "foo"
fs := Dir(testRoot, true)
assert.Equal(t, http.Dir(testRoot), fs)
}
func TestDir(t *testing.T) {
testRoot := "foo"
fs := Dir(testRoot, false)
assert.Equal(t, &OnlyFilesFS{FileSystem: http.Dir(testRoot)}, fs)
}

371
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,54 +11,29 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"regexp"
"strings" "strings"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
filesystem "github.com/gin-gonic/gin/internal/fs"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/quic-go/quic-go/http3"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
) )
const defaultMultipartMemory = 32 << 20 // 32 MB const defaultMultipartMemory = 32 << 20 // 32 MB
const escapedColon = "\\:"
const colon = ":"
const backslash = "\\"
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")
) )
var defaultPlatform string var defaultAppEngine bool
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)
// OptionFunc defines the function to change the default configuration // HandlersChain defines a HandlerFunc array.
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. i.e. the last handler is the main one. // Last returns the last handler in the chain. ie. 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]
@ -74,34 +49,22 @@ type RouteInfo struct {
HandlerFunc HandlerFunc HandlerFunc HandlerFunc
} }
// RoutesInfo defines a RouteInfo slice. // RoutesInfo defines a RouteInfo array.
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
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a // 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
// RedirectFixedPath if enabled, the router tries to fix the current request path, if no // 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.
@ -112,7 +75,7 @@ type Engine struct {
// RedirectTrailingSlash is independent of this option. // RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool RedirectFixedPath bool
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the // 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.
@ -120,49 +83,43 @@ type Engine struct {
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
// ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that // If enabled, client IP will be parsed from the request's headers that
// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was // match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
// fetched, it falls back to the IP obtained from // fetched, it falls back to the IP obtained from
// `(*gin.Context).Request.RemoteAddr`. // `(*gin.Context).Request.RemoteAddr`.
ForwardedByClientIP bool ForwardedByClientIP bool
// AppEngine was deprecated. // List of headers used to obtain the client IP when
// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD // `(*gin.Engine).ForwardedByClientIP` is `true` and
// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
// network origins of `(*gin.Engine).TrustedProxies`.
RemoteIPHeaders []string
// 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 []string
// #726 #755 If enabled, it will trust some headers starting with // #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
// UseRawPath if enabled, the url.RawPath will be used to find parameters. // If enabled, the url.RawPath will be used to find parameters.
UseRawPath bool UseRawPath bool
// UnescapePathValues if true, the path value will be unescaped. // 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
// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes. // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// 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. // RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
UseH2C bool // See the PR #1817 and issue #1644
RemoveExtraSlash 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
@ -175,22 +132,20 @@ type Engine struct {
pool sync.Pool pool sync.Pool
trees methodTrees trees methodTrees
maxParams uint16 maxParams uint16
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet trustedCIDRs []*net.IPNet
} }
var _ IRouter = (*Engine)(nil) var _ IRouter = &Engine{}
// 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(opts ...OptionFunc) *Engine { func New() *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
engine := &Engine{ engine := &Engine{
RouterGroup: RouterGroup{ RouterGroup: RouterGroup{
@ -204,7 +159,8 @@ func New(opts ...OptionFunc) *Engine {
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform, TrustedProxies: []string{"0.0.0.0/0"},
AppEngine: defaultAppEngine,
UseRawPath: false, UseRawPath: false,
RemoveExtraSlash: false, RemoveExtraSlash: false,
UnescapePathValues: true, UnescapePathValues: true,
@ -212,40 +168,28 @@ func New(opts ...OptionFunc) *Engine {
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.engine = engine engine.RouterGroup.engine = engine
engine.pool.New = func() any { engine.pool.New = func() interface{} {
return engine.allocateContext(engine.maxParams) return engine.allocateContext()
} }
return engine.With(opts...) return engine
} }
// 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(opts ...OptionFunc) *Engine { func Default() *Engine {
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
engine := New() engine := New()
engine.Use(Logger(), Recovery()) engine.Use(Logger(), Recovery())
return engine.With(opts...) return engine
} }
func (engine *Engine) Handler() http.Handler { func (engine *Engine) allocateContext() *Context {
if !engine.UseH2C { v := make(Params, 0, engine.maxParams)
return engine return &Context{engine: engine, params: &v}
}
h2s := &http2.Server{}
return h2c.NewHandler(engine, h2s)
} }
func (engine *Engine) allocateContext(maxParams uint16) *Context { // Delims sets template left and right delims and returns a Engine instance.
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
@ -285,19 +229,6 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
filesystem.FileSystem{FileSystem: fs}, patterns...))
engine.SetHTMLTemplate(templ)
}
// SetHTMLTemplate associate a template with HTML renderer. // SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {
@ -312,19 +243,19 @@ func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.FuncMap = funcMap engine.FuncMap = funcMap
} }
// NoRoute adds handlers for NoRoute. It returns a 404 code by default. // NoRoute adds handlers for NoRoute. It return 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 Engine.HandleMethodNotAllowed = true. // NoMethod sets the handlers called when... TODO.
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. i.e. the middleware attached through Use() will be // Use attaches a global middleware to the router. ie. the middleware attached though 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 {
@ -334,15 +265,6 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine return engine
} }
// With returns an 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)
} }
@ -366,17 +288,14 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
} }
root.addRoute(path, handlers) root.addRoute(path, handlers)
// Update maxParams
if paramsCount := countParams(path); paramsCount > engine.maxParams { if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount 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:
// the http method, path, and the handler name. // the http method, path and the handler name.
func (engine *Engine) Routes() (routes RoutesInfo) { func (engine *Engine) Routes() (routes RoutesInfo) {
for _, tree := range engine.trees { for _, tree := range engine.trees {
routes = iterate("", tree.method, routes, tree.root) routes = iterate("", tree.method, routes, tree.root)
@ -401,13 +320,30 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
return routes return routes
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
trustedCIDRs, err := engine.prepareTrustedCIDRs()
if err != nil {
return err
}
engine.trustedCIDRs = trustedCIDRs
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) { func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.trustedProxies == nil { if engine.TrustedProxies == nil {
return nil, nil return nil, nil
} }
cidr := make([]*net.IPNet, 0, len(engine.trustedProxies)) cidr := make([]*net.IPNet, 0, len(engine.TrustedProxies))
for _, trustedProxy := range engine.trustedProxies { for _, trustedProxy := range engine.TrustedProxies {
if !strings.Contains(trustedProxy, "/") { if !strings.Contains(trustedProxy, "/") {
ip := parseIP(trustedProxy) ip := parseIP(trustedProxy)
if ip == nil { if ip == nil {
@ -430,86 +366,6 @@ func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
return cidr, nil 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
}
// updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
n.fullPath = strings.ReplaceAll(n.fullPath, escapedColon, colon)
n.indices = strings.ReplaceAll(n.indices, backslash, colon)
if n.children == nil {
return
}
for _, child := range n.children {
updateRouteTree(child)
}
}
// updateRouteTrees do update to the route trees
func (engine *Engine) updateRouteTrees() {
for _, tree := range engine.trees {
updateRouteTree(tree.root)
}
}
// parseIP parse a string representation of an IP and returns a net.IP with the // parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid. // minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP { func parseIP(ip string) net.IP {
@ -524,23 +380,6 @@ func parseIP(ip string) net.IP {
return parsedIP return parsedIP
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
// 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.
@ -548,27 +387,17 @@ 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) }()
if engine.isUnsafeTrustedProxies() { err = http.ListenAndServeTLS(addr, certFile, keyFile, engine)
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 (i.e. a file). // through the specified unix socket (ie. 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) }()
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
@ -576,7 +405,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
defer listener.Close() defer listener.Close()
defer os.Remove(file) defer os.Remove(file)
err = http.Serve(listener, engine.Handler()) err = http.Serve(listener, engine)
return return
} }
@ -587,11 +416,6 @@ 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 {
@ -602,34 +426,12 @@ func (engine *Engine) RunFd(fd int) (err error) {
return return
} }
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving QUIC on %s\n", addr)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests // RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener // through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) { func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr()) debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
err = http.Serve(listener, engine)
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.Serve(listener, engine.Handler())
return return
} }
@ -645,17 +447,15 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c) engine.pool.Put(c)
} }
// HandleContext re-enters a context that has been rewritten. // HandleContext re-enter 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 deal with this, use wisely. // Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index oldIndexValue := c.index
oldHandlers := c.handlers
c.reset() c.reset()
engine.handleHTTPRequest(c) engine.handleHTTPRequest(c)
c.index = oldIndexValue c.index = oldIndexValue
c.handlers = oldHandlers
} }
func (engine *Engine) handleHTTPRequest(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
@ -679,7 +479,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
} }
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape) value := root.getValue(rPath, c.params, unescape)
if value.params != nil { if value.params != nil {
c.Params = *value.params c.Params = *value.params
} }
@ -690,7 +490,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return return
} }
if httpMethod != http.MethodConnect && rPath != "/" { if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash { if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c) redirectTrailingSlash(c)
return return
@ -702,26 +502,18 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
break break
} }
if engine.HandleMethodNotAllowed && len(t) > 0 { 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 value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil { if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
allowed = append(allowed, tree.method) c.handlers = engine.allNoMethod
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)
} }
@ -749,9 +541,6 @@ 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
} }
req.URL.Path = p + "/" req.URL.Path = p + "/"

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,10 +12,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var ( var once sync.Once
once sync.Once var internalEngine *gin.Engine
internalEngine *gin.Engine
)
func engine() *gin.Engine { func engine() *gin.Engine {
once.Do(func() { once.Do(func() {
@ -34,17 +32,12 @@ func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...) engine().LoadHTMLFiles(files...)
} }
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
engine().LoadHTMLFS(fs, patterns...)
}
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate. // SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) { func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ) engine().SetHTMLTemplate(templ)
} }
// NoRoute adds handlers for NoRoute. It returns a 404 code by default. // NoRoute adds handlers for NoRoute. It return a 404 code by default.
func NoRoute(handlers ...gin.HandlerFunc) { func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...) engine().NoRoute(handlers...)
} }
@ -115,8 +108,7 @@ 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)
} }
@ -126,7 +118,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. i.e. the middlewares attached through Use() will be // Use attaches a global middleware to the router. ie. the middlewares attached though 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 {
@ -153,7 +145,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 (i.e. a file) // through the specified unix socket (ie. 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)
@ -161,7 +153,7 @@ func RunUnix(file string) (err error) {
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests // RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor. // through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless an error happens. // Note: the method will block the calling goroutine indefinitely unless on error happens.
func RunFd(fd int) (err error) { func RunFd(fd int) (err error) {
return engine().RunFd(fd) return engine().RunFd(fd)
} }

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,29 +9,20 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"io" "io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty) func testRequest(t *testing.T, url string) {
// 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,
@ -39,27 +30,14 @@ func testRequest(t *testing.T, params ...string) {
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get(params[0]) resp, err := client.Get(url)
require.NoError(t, err) assert.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
body, ioerr := io.ReadAll(resp.Body) body, ioerr := ioutil.ReadAll(resp.Body)
require.NoError(t, ioerr) assert.NoError(t, ioerr)
assert.Equal(t, "it worked", string(body), "resp body should match")
responseStatus := "200 OK" assert.Equal(t, "200 OK", resp.Status, "should get a 200")
if len(params) > 1 && params[1] != "" {
responseStatus = params[1]
}
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) {
@ -73,85 +51,17 @@ func TestRunEmpty(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.Run(":8080")) assert.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestBadTrustedCIDRs(t *testing.T) { func TestTrustedCIDRsForRun(t *testing.T) {
router := New()
require.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
}
/* legacy tests
func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "") os.Setenv("PORT", "")
router := New() router := New()
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
require.Error(t, router.Run(":8080")) 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") })
require.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")
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err)
socketFile, err := listener.File()
require.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
require.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")
require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
require.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"}
require.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() {
@ -164,12 +74,12 @@ func TestRunTLS(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example") testRequest(t, "https://localhost:8443/example")
} }
func TestPusher(t *testing.T) { func TestPusher(t *testing.T) {
html := template.Must(template.New("https").Parse(` var html = template.Must(template.New("https").Parse(`
<html> <html>
<head> <head>
<title>Https Test</title> <title>Https Test</title>
@ -201,7 +111,7 @@ func TestPusher(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher") testRequest(t, "https://localhost:8449/pusher")
} }
@ -216,14 +126,14 @@ func TestRunEmptyWithEnv(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.Run(":3123")) assert.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example") testRequest(t, "http://localhost:3123/example")
} }
func TestRunTooMuchParams(t *testing.T) { func TestRunTooMuchParams(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
require.NoError(t, router.Run("2", "2")) assert.NoError(t, router.Run("2", "2"))
}) })
} }
@ -237,7 +147,7 @@ func TestRunWithPort(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
require.Error(t, router.Run(":5150")) assert.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example") testRequest(t, "http://localhost:5150/example")
} }
@ -257,7 +167,7 @@ func TestUnixSocket(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", unixTestSocket) c, err := net.Dial("unix", unixTestSocket)
require.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")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -271,43 +181,18 @@ func TestUnixSocket(t *testing.T) {
func TestBadUnixSocket(t *testing.T) { func TestBadUnixSocket(t *testing.T) {
router := New() router := New()
require.Error(t, router.RunUnix("#/tmp/unix_unit_test")) assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
}
func TestRunQUIC(t *testing.T) {
router := New()
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
} }
func TestFileDescriptor(t *testing.T) { func TestFileDescriptor(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
socketFile, err := listener.File() socketFile, err := listener.File()
if isWindows() { assert.NoError(t, err)
// not supported by windows, it is unimplemented now
require.Error(t, err)
} else {
require.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") })
@ -318,7 +203,7 @@ func TestFileDescriptor(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String()) c, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err) assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -332,15 +217,15 @@ func TestFileDescriptor(t *testing.T) {
func TestBadFileDescriptor(t *testing.T) { func TestBadFileDescriptor(t *testing.T) {
router := New() router := New()
require.Error(t, router.RunFd(0)) assert.Error(t, router.RunFd(0))
} }
func TestListener(t *testing.T) { func TestListener(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
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.RunListener(listener)) assert.NoError(t, router.RunListener(listener))
@ -350,7 +235,7 @@ func TestListener(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String()) c, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err) assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -365,11 +250,11 @@ func TestListener(t *testing.T) {
func TestBadListener(t *testing.T) { func TestBadListener(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086") addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
require.NoError(t, err) assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
require.NoError(t, err) assert.NoError(t, err)
listener.Close() listener.Close()
require.Error(t, router.RunListener(listener)) assert.Error(t, router.RunListener(listener))
} }
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
@ -395,14 +280,7 @@ func TestConcurrentHandleContext(t *testing.T) {
wg.Add(iterations) wg.Add(iterations)
for i := 0; i < iterations; i++ { for i := 0; i < iterations; i++ {
go func() { go func() {
req, err := http.NewRequest(http.MethodGet, "/", nil) testGetRequestHandler(t, router, "/")
assert.NoError(t, err)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
wg.Done() wg.Done()
}() }()
} }
@ -424,177 +302,13 @@ func TestConcurrentHandleContext(t *testing.T) {
// testRequest(t, "http://localhost:8033/example") // testRequest(t, "http://localhost:8033/example")
// } // }
func TestTreeRunDynamicRouting(t *testing.T) { func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
router := New() req, err := http.NewRequest(http.MethodGet, url, nil)
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") }) assert.NoError(t, err)
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) w := httptest.NewRecorder()
defer ts.Close() h.ServeHTTP(w, req)
testRequest(t, ts.URL+"/", "", "home") assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx") assert.Equal(t, 200, w.Code, "should get a 200")
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"
}
func TestEscapedColon(t *testing.T) {
router := New()
f := func(u string) {
router.GET(u, func(c *Context) { c.String(http.StatusOK, u) })
}
f("/r/r\\:r")
f("/r/r:r")
f("/r/r/:r")
f("/r/r/\\:r")
f("/r/r/r\\:r")
assert.Panics(t, func() {
f("\\foo:")
})
router.updateRouteTrees()
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/r/r123", "", "/r/r:r")
testRequest(t, ts.URL+"/r/r:r", "", "/r/r\\:r")
testRequest(t, ts.URL+"/r/r/r123", "", "/r/r/:r")
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
} }

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,20 +8,17 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"io" "io/ioutil"
"net" "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"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
) )
func formatAsDate(t time.Time) string { func formatAsDate(t time.Time) string {
@ -45,8 +42,8 @@ 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]any{ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), //nolint:gofumpt "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
}) })
@ -73,50 +70,12 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.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))
} }
@ -131,12 +90,12 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -151,12 +110,12 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -178,12 +137,12 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -198,13 +157,13 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/raw") res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp)) assert.Equal(t, "Date: 2017/07/01\n", string(resp))
} }
func init() { func init() {
@ -229,12 +188,12 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -249,12 +208,12 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -269,12 +228,12 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/test") res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -296,12 +255,12 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
@ -316,151 +275,42 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/raw") res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
resp, _ := io.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp)) assert.Equal(t, "Date: 2017/07/01\n", string(resp))
}
var tmplFS = http.Dir("testdata/template")
func TestLoadHTMLFSTestMode(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSReleaseMode(t *testing.T) {
ts := setupHTMLFiles(
t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSUsingTLS(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSFuncMap(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
} }
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
router := New() router := New()
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}}) router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1) assert.Len(t, router.trees, 1)
assert.NotNil(t, router.trees.get(http.MethodGet)) assert.NotNil(t, router.trees.get("GET"))
assert.Nil(t, router.trees.get(http.MethodPost)) assert.Nil(t, router.trees.get("POST"))
router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2) assert.Len(t, router.trees, 2)
assert.NotNil(t, router.trees.get(http.MethodGet)) assert.NotNil(t, router.trees.get("GET"))
assert.NotNil(t, router.trees.get(http.MethodPost)) assert.NotNil(t, router.trees.get("POST"))
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2) assert.Len(t, router.trees, 2)
} }
func TestAddRouteFails(t *testing.T) { func TestAddRouteFails(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) }) assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "a", HandlersChain{func(_ *Context) {}}) }) assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "/", HandlersChain{}) }) assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
assert.Panics(t, func() { assert.Panics(t, func() {
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}}) router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
}) })
} }
@ -545,6 +395,7 @@ 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) {
@ -578,7 +429,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 any) { func compareFunc(t *testing.T, a, b interface{}) {
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() {
@ -602,27 +453,27 @@ func TestListOfRoutes(t *testing.T) {
assert.Len(t, list, 7) assert.Len(t, list, 7)
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/favicon.ico", Path: "/favicon.ico",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/users/", Path: "/users/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodGet, Method: "GET",
Path: "/users/:id", Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: http.MethodPost, Method: "POST",
Path: "/users/:id", Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
}) })
@ -640,7 +491,7 @@ func TestEngineHandleContext(t *testing.T) {
} }
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/") w := performRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code) assert.Equal(t, 301, w.Code)
}) })
} }
@ -657,10 +508,10 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
r.GET("/:count", func(c *Context) { r.GET("/:count", func(c *Context) {
countStr := c.Param("count") countStr := c.Param("count")
count, err := strconv.Atoi(countStr) count, err := strconv.Atoi(countStr)
require.NoError(t, err) assert.NoError(t, err)
n, err := c.Writer.Write([]byte(".")) n, err := c.Writer.Write([]byte("."))
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, n) assert.Equal(t, 1, n)
switch { switch {
@ -673,7 +524,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
}) })
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/"+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())
}) })
@ -682,110 +533,87 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter) assert.Equal(t, int64(expectValue), middlewareCounter)
} }
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
// given
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
r := New()
v1 := r.Group("/v1")
{
v1.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounterV1, 1)
})
v1.GET("/test", func(c *Context) {
atomic.AddInt64(&handlerCounterV1, 1)
c.Status(http.StatusOK)
})
}
v2 := r.Group("/v2")
{
v2.GET("/test", func(c *Context) {
c.Request.URL.Path = "/v1/test"
r.HandleContext(c)
}, func(c *Context) {
atomic.AddInt64(&handlerCounterV2, 1)
})
}
// when
responseV1 := PerformRequest(r, "GET", "/v1/test")
responseV2 := PerformRequest(r, "GET", "/v2/test")
// then
assert.Equal(t, 200, responseV1.Code)
assert.Equal(t, 200, responseV2.Code)
assert.Equal(t, int64(2), handlerCounterV1)
assert.Equal(t, int64(2), middlewareCounterV1)
assert.Equal(t, int64(1), handlerCounterV2)
}
func TestPrepareTrustedCIRDsWith(t *testing.T) { func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New() r := New()
// valid ipv4 cidr // valid ipv4 cidr
{ {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
err := r.SetTrustedProxies([]string{"0.0.0.0/0"}) r.TrustedProxies = []string{"0.0.0.0/0"}
require.NoError(t, err) trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
} }
// invalid ipv4 cidr // invalid ipv4 cidr
{ {
err := r.SetTrustedProxies([]string{"192.168.1.33/33"}) r.TrustedProxies = []string{"192.168.1.33/33"}
require.Error(t, err) _, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
} }
// valid ipv4 address // valid ipv4 address
{ {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("192.168.1.33/32")}
r.TrustedProxies = []string{"192.168.1.33"}
err := r.SetTrustedProxies([]string{"192.168.1.33"}) trustedCIDRs, err := r.prepareTrustedCIDRs()
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
} }
// invalid ipv4 address // invalid ipv4 address
{ {
err := r.SetTrustedProxies([]string{"192.168.1.256"}) r.TrustedProxies = []string{"192.168.1.256"}
require.Error(t, err) _, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
} }
// valid ipv6 address // valid ipv6 address
{ {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")} 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"}) r.TrustedProxies = []string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"}
require.NoError(t, err) trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
} }
// invalid ipv6 address // invalid ipv6 address
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}) r.TrustedProxies = []string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}
require.Error(t, err) _, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
} }
// valid ipv6 cidr // valid ipv6 cidr
{ {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"}) r.TrustedProxies = []string{"::/0"}
require.NoError(t, err) trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
} }
// invalid ipv6 cidr // invalid ipv6 cidr
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}) r.TrustedProxies = []string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}
require.Error(t, err) _, err := r.prepareTrustedCIDRs()
assert.Error(t, err)
} }
// valid combination // valid combination
@ -795,34 +623,39 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
parseCIDR("192.168.0.0/16"), parseCIDR("192.168.0.0/16"),
parseCIDR("172.16.0.1/32"), parseCIDR("172.16.0.1/32"),
} }
err := r.SetTrustedProxies([]string{ r.TrustedProxies = []string{
"::/0", "::/0",
"192.168.0.0/16", "192.168.0.0/16",
"172.16.0.1", "172.16.0.1",
}) }
require.NoError(t, err) trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, trustedCIDRs)
} }
// invalid combination // invalid combination
{ {
err := r.SetTrustedProxies([]string{ r.TrustedProxies = []string{
"::/0", "::/0",
"192.168.0.0/16", "192.168.0.0/16",
"172.16.0.256", "172.16.0.256",
}) }
_, err := r.prepareTrustedCIDRs()
require.Error(t, err) assert.Error(t, err)
} }
// nil value // nil value
{ {
err := r.SetTrustedProxies(nil) r.TrustedProxies = nil
trustedCIDRs, err := r.prepareTrustedCIDRs()
assert.Nil(t, r.trustedCIDRs) assert.Nil(t, trustedCIDRs)
require.NoError(t, err) assert.Nil(t, err)
} }
} }
func parseCIDR(cidr string) *net.IPNet { func parseCIDR(cidr string) *net.IPNet {
@ -845,71 +678,3 @@ 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) {
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: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, 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: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
}
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.ReplaceAll(param, "-", "/"))
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(http.MethodGet, "/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())
}
// Test the fix for https://github.com/gin-gonic/gin/issues/4002
func TestMethodNotAllowedNoRoute(t *testing.T) {
g := New()
g.HandleMethodNotAllowed = true
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}

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"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"strconv"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type route struct { type route struct {
@ -297,14 +295,14 @@ func TestShouldBindUri(t *testing.T) {
} }
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
require.NoError(t, c.ShouldBindUri(&person)) assert.NoError(t, c.ShouldBindUri(&person))
assert.NotEmpty(t, person.Name) assert.True(t, "" != person.Name)
assert.NotEmpty(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, http.MethodGet, 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)
} }
@ -319,14 +317,14 @@ func TestBindUri(t *testing.T) {
} }
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
require.NoError(t, c.BindUri(&person)) assert.NoError(t, c.BindUri(&person))
assert.NotEmpty(t, person.Name) assert.True(t, "" != person.Name)
assert.NotEmpty(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, http.MethodGet, 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)
} }
@ -340,11 +338,11 @@ func TestBindUriError(t *testing.T) {
} }
router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) { router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member var m Member
require.Error(t, c.BindUri(&m)) assert.Error(t, c.BindUri(&m))
}) })
path1, _ := exampleFromPath("/new/rest/:num") path1, _ := exampleFromPath("/new/rest/:num")
w1 := PerformRequest(router, http.MethodGet, path1) w1 := performRequest(router, http.MethodGet, path1)
assert.Equal(t, http.StatusBadRequest, w1.Code) assert.Equal(t, http.StatusBadRequest, w1.Code)
} }
@ -360,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, http.MethodGet, "/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())
} }
@ -391,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\"")
@ -403,7 +401,7 @@ func TestGithubAPI(t *testing.T) {
} }
func exampleFromPath(path string) (string, Params) { func exampleFromPath(path string) (string, Params) {
output := new(strings.Builder) output := new(bytes.Buffer)
params := make(Params, 0, 6) params := make(Params, 0, 6)
start := -1 start := -1
for i, c := range path { for i, c := range path {
@ -412,7 +410,7 @@ func exampleFromPath(path string) (string, Params) {
} }
if start >= 0 { if start >= 0 {
if c == '/' { if c == '/' {
value := strconv.Itoa(rand.Intn(100000)) value := fmt.Sprint(rand.Intn(100000))
params = append(params, Param{ params = append(params, Param{
Key: path[start:i], Key: path[start:i],
Value: value, Value: value,
@ -426,7 +424,7 @@ func exampleFromPath(path string) (string, Params) {
} }
} }
if start >= 0 { if start >= 0 {
value := strconv.Itoa(rand.Intn(100000)) value := fmt.Sprint(rand.Intn(100000))
params = append(params, Param{ params = append(params, Param{
Key: path[start:], Key: path[start:],
Value: value, Value: value,

51
go.mod
View File

@ -1,47 +1,14 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.23.0 go 1.13
require ( require (
github.com/bytedance/sonic v1.13.2 github.com/gin-contrib/sse v0.1.0
github.com/gin-contrib/sse v1.1.0 github.com/go-playground/validator/v10 v10.4.1
github.com/go-playground/validator/v10 v10.26.0 github.com/golang/protobuf v1.3.3
github.com/goccy/go-json v0.10.2 github.com/json-iterator/go v1.1.9
github.com/goccy/go-yaml v1.18.0 github.com/mattn/go-isatty v0.0.12
github.com/json-iterator/go v1.1.12 github.com/stretchr/testify v1.4.0
github.com/mattn/go-isatty v0.0.20 github.com/ugorji/go/codec v1.1.7
github.com/modern-go/reflect2 v1.0.2 gopkg.in/yaml.v2 v2.2.8
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.52.0
github.com/stretchr/testify v1.10.0
github.com/ugorji/go/codec v1.2.12
golang.org/x/net v0.41.0
google.golang.org/protobuf v1.36.6
)
require (
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

138
go.sum
View File

@ -1,110 +1,52 @@
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -1,4 +1,4 @@
// Copyright 2023 Gin Core Team. All rights reserved. // Copyright 2020 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,13 +9,16 @@ import (
) )
// StringToBytes converts string to byte slice without a memory allocation. // 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 { func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s)) return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
} }
// BytesToString converts byte slice to string without a memory allocation. // 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 { func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b)) return *(*string)(unsafe.Pointer(&b))
} }

View File

@ -6,17 +6,14 @@ package bytesconv
import ( import (
"bytes" "bytes"
cRand "crypto/rand"
"math/rand" "math/rand"
"strings" "strings"
"testing" "testing"
"time" "time"
) )
var ( var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere." var testBytes = []byte(testString)
testBytes = []byte(testString)
)
func rawBytesToStr(b []byte) string { func rawBytesToStr(b []byte) string {
return string(b) return string(b)
@ -31,10 +28,7 @@ func rawStrToBytes(s string) []byte {
func TestBytesToString(t *testing.T) { func TestBytesToString(t *testing.T) {
data := make([]byte, 1024) data := make([]byte, 1024)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
_, err := cRand.Read(data) rand.Read(data)
if err != nil {
t.Fatal(err)
}
if rawBytesToStr(data) != BytesToString(data) { if rawBytesToStr(data) != BytesToString(data) {
t.Fatal("don't match") t.Fatal("don't match")
} }
@ -48,7 +42,7 @@ const (
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
) )
var src = rand.New(rand.NewSource(time.Now().UnixNano())) var src = rand.NewSource(time.Now().UnixNano())
func RandStringBytesMaskImprSrcSB(n int) string { func RandStringBytesMaskImprSrcSB(n int) string {
sb := strings.Builder{} sb := strings.Builder{}

View File

@ -1,21 +0,0 @@
package fs
import (
"io/fs"
"net/http"
)
// FileSystem implements an [fs.FS].
type FileSystem struct {
http.FileSystem
}
// Open passes `Open` to the upstream implementation and return an [fs.File].
func (o FileSystem) Open(name string) (fs.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
return fs.File(f), nil
}

View File

@ -1,49 +0,0 @@
package fs
import (
"errors"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
open func(name string) (http.File, error)
}
func (m *mockFileSystem) Open(name string) (http.File, error) {
return m.open(name)
}
func TestFileSystem_Open(t *testing.T) {
var testFile *os.File
mockFS := &mockFileSystem{
open: func(name string) (http.File, error) {
return testFile, nil
},
}
fs := &FileSystem{mockFS}
file, err := fs.Open("foo")
require.NoError(t, err)
assert.Equal(t, testFile, file)
}
func TestFileSystem_Open_err(t *testing.T) {
testError := errors.New("mock")
mockFS := &mockFileSystem{
open: func(_ string) (http.File, error) {
return nil, testError
},
}
fs := &FileSystem{mockFS}
file, err := fs.Open("foo")
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}

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

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

24
internal/json/jsoniter.go Normal file
View File

@ -0,0 +1,24 @@
// 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 jsoniter
// +build jsoniter
package json
import jsoniter "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
// 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,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.
@ -44,18 +44,11 @@ type LoggerConfig struct {
// Optional. Default value is gin.DefaultWriter. // Optional. Default value is gin.DefaultWriter.
Output io.Writer Output io.Writer
// SkipPaths is an url path array which logs are not written. // SkipPaths is a 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
@ -77,12 +70,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 gin's output descriptor refers to a terminal. // isTerm shows whether does 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[any]any Keys map[string]interface{}
} }
// 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.
@ -90,8 +83,6 @@ 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:
@ -147,7 +138,8 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
} }
if param.Latency > time.Minute { if param.Latency > time.Minute {
param.Latency = param.Latency.Truncate(time.Second) // Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
} }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\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"),
@ -170,12 +162,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()
@ -187,7 +179,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{})
} }
@ -248,34 +240,32 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
// Process request // Process request
c.Next() c.Next()
// Log only when it is not being skipped // Log only when path is not being skipped
if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) { if _, ok := skip[path]; !ok {
return param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
fmt.Fprint(out, formatter(param))
} }
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(strings.Builder) buffer := new(bytes.Buffer)
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,9 +31,9 @@ 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, http.MethodGet, "/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(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -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, http.MethodPost, "/example") performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPost) assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodPut, "/example") performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPut) assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodDelete, "/example") performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodDelete) 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, http.MethodGet, "/notfound") performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), http.MethodGet) 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(strings.Builder) buffer := new(bytes.Buffer)
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,9 +95,9 @@ 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, http.MethodGet, "/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(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -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, http.MethodPost, "/example") performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPost) assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodPut, "/example") performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodPut) assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodDelete, "/example") performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodDelete) 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, http.MethodGet, "/notfound") performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), http.MethodGet) 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(strings.Builder) buffer := new(bytes.Buffer)
d := DefaultWriter d := DefaultWriter
DefaultWriter = buffer DefaultWriter = buffer
@ -169,24 +169,22 @@ func TestLoggerWithFormatter(t *testing.T) {
) )
})) }))
router.GET("/example", func(c *Context) {}) router.GET("/example", func(c *Context) {})
PerformRequest(router, http.MethodGet, "/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]")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
} }
func TestLoggerWithConfigFormatting(t *testing.T) { func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams var gotParam LogFormatterParams
var gotKeys map[any]any var gotKeys map[string]interface{}
buffer := new(strings.Builder) buffer := new(bytes.Buffer)
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 {
@ -208,14 +206,13 @@ 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, http.MethodGet, "/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]")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), http.MethodGet) assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -225,10 +222,11 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, 200, gotParam.StatusCode) assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency) assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP) assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, http.MethodGet, gotParam.Method) assert.Equal(t, "GET", gotParam.Method)
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) {
@ -239,7 +237,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Second * 5, Latency: time.Second * 5,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: false, isTerm: false,
@ -250,7 +248,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Second * 5, Latency: time.Second * 5,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: true, isTerm: true,
@ -260,7 +258,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Millisecond * 9876543210, Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: true, isTerm: true,
@ -271,7 +269,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Millisecond * 9876543210, Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: http.MethodGet, Method: "GET",
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: false, isTerm: false,
@ -282,6 +280,7 @@ func TestDefaultLogFormatter(t *testing.T) {
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 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| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
@ -292,10 +291,10 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor() return p.MethodColor()
} }
assert.Equal(t, blue, colorForMethod(http.MethodGet), "get should be blue") assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, cyan, colorForMethod(http.MethodPost), "post should be cyan") assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, yellow, colorForMethod(http.MethodPut), "put should be yellow") assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, red, colorForMethod(http.MethodDelete), "delete should be red") assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green") assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white") assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
@ -310,7 +309,6 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor() return p.StatusCodeColor()
} }
assert.Equal(t, white, colorForStatus(http.StatusContinue), "1xx should be white")
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow") assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
@ -329,13 +327,13 @@ func TestIsOutputColor(t *testing.T) {
} }
consoleColorMode = autoColor consoleColorMode = autoColor
assert.True(t, p.IsOutputColor()) assert.Equal(t, true, p.IsOutputColor())
ForceConsoleColor() ForceConsoleColor()
assert.True(t, p.IsOutputColor()) assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor() DisableConsoleColor()
assert.False(t, p.IsOutputColor()) assert.Equal(t, false, p.IsOutputColor())
// test with isTerm flag false. // test with isTerm flag false.
p = LogFormatterParams{ p = LogFormatterParams{
@ -343,13 +341,13 @@ func TestIsOutputColor(t *testing.T) {
} }
consoleColorMode = autoColor consoleColorMode = autoColor
assert.False(t, p.IsOutputColor()) assert.Equal(t, false, p.IsOutputColor())
ForceConsoleColor() ForceConsoleColor()
assert.True(t, p.IsOutputColor()) assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor() DisableConsoleColor()
assert.False(t, p.IsOutputColor()) assert.Equal(t, false, p.IsOutputColor())
// reset console color mode. // reset console color mode.
consoleColorMode = autoColor consoleColorMode = autoColor
@ -359,46 +357,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, http.MethodGet, "/error") w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, "{\"error\":\"this is an error\"}", w.Body.String()) assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/abort") w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.JSONEq(t, "{\"error\":\"no authorized\"}", w.Body.String()) assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/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(strings.Builder) buffer := new(bytes.Buffer)
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, http.MethodGet, "/logged") performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodGet, "/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(strings.Builder) buffer := new(bytes.Buffer)
router := New() router := New()
router.Use(LoggerWithConfig(LoggerConfig{ router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer, Output: buffer,
@ -407,31 +405,11 @@ 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, http.MethodGet, "/logged") performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodGet, "/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, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, http.MethodGet, "/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, http.MethodGet, "/") 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, http.MethodGet, "/") 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, http.MethodGet, "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -118,10 +118,7 @@ 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()
@ -147,9 +144,8 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
router.POST("/", func(c *Context) { router.POST("/", func(c *Context) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
@ -175,7 +171,7 @@ func TestMiddlewareAbort(t *testing.T) {
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
@ -190,20 +186,21 @@ 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, http.MethodGet, "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusGone, w.Code) assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature) assert.Equal(t, "ACB", signature)
} }
// TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as // TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// as well as Abort // as well as Abort
func TestMiddlewareFailHandlersChain(t *testing.T) { func TestMiddlewareFailHandlersChain(t *testing.T) {
// SETUP // SETUP
@ -211,7 +208,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"
@ -219,7 +216,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C" signature += "C"
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
@ -246,8 +243,8 @@ func TestMiddlewareWrite(t *testing.T) {
}) })
}) })
w := PerformRequest(router, http.MethodGet, "/") w := performRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", "")) 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))
} }

36
mode.go
View File

@ -1,14 +1,12 @@
// 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"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
) )
@ -36,18 +34,15 @@ 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"
// import "github.com/mattn/go-colorable" // gin.DefaultWriter = colorable.NewColorableStdout()
// 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
var DefaultErrorWriter io.Writer = os.Stderr var DefaultErrorWriter io.Writer = os.Stderr
var ( var ginMode = debugCode
ginMode int32 = debugCode var modeName = DebugMode
modeName atomic.Value
)
func init() { func init() {
mode := os.Getenv(EnvGinMode) mode := os.Getenv(EnvGinMode)
@ -57,24 +52,21 @@ 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 value == "" {
if flag.Lookup("test.v") != nil { value = DebugMode
value = TestMode
} else {
value = DebugMode
}
} }
switch value { switch value {
case DebugMode, "": case DebugMode:
atomic.StoreInt32(&ginMode, debugCode) ginMode = debugCode
case ReleaseMode: case ReleaseMode:
atomic.StoreInt32(&ginMode, releaseCode) ginMode = releaseCode
case TestMode: case TestMode:
atomic.StoreInt32(&ginMode, testCode) ginMode = testCode
default: default:
panic("gin mode unknown: " + value + " (available mode: debug release test)") panic("gin mode unknown: " + value + " (available mode: debug release test)")
} }
modeName.Store(value)
modeName = value
} }
// DisableBindValidation closes the default validator. // DisableBindValidation closes the default validator.
@ -94,7 +86,7 @@ func EnableJsonDecoderDisallowUnknownFields() {
binding.EnableDecoderDisallowUnknownFields = true binding.EnableDecoderDisallowUnknownFields = true
} }
// Mode returns current gin mode. // Mode returns currently gin mode.
func Mode() string { func Mode() string {
return modeName.Load().(string) return modeName
} }

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,6 @@ package gin
import ( import (
"os" "os"
"sync/atomic"
"testing" "testing"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -18,24 +17,24 @@ func init() {
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
os.Unsetenv(EnvGinMode) os.Unsetenv(EnvGinMode)
SetMode("") SetMode("")
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, debugCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, DebugMode, Mode())
SetMode(DebugMode) SetMode(DebugMode)
assert.Equal(t, int32(debugCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode()) assert.Equal(t, DebugMode, Mode())
SetMode(ReleaseMode) SetMode(ReleaseMode)
assert.Equal(t, int32(releaseCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, releaseCode, ginMode)
assert.Equal(t, ReleaseMode, Mode()) assert.Equal(t, ReleaseMode, Mode())
SetMode(TestMode) SetMode(TestMode)
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode)) assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })

12
path.go
View File

@ -10,12 +10,12 @@ 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 {

View File

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

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"
@ -19,19 +19,22 @@ import (
"time" "time"
) )
const dunno = "???" var (
dunno = []byte("???")
var dunnoBytes = []byte(dunno) centerDot = []byte("·")
dot = []byte(".")
slash = []byte("/")
)
// RecoveryFunc defines the function passable to CustomRecovery. // RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any) type RecoveryFunc func(c *Context, err interface{})
// 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. //CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
func CustomRecovery(handle RecoveryFunc) HandlerFunc { func CustomRecovery(handle RecoveryFunc) HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter, handle) return RecoveryWithWriter(DefaultErrorWriter, handle)
} }
@ -57,11 +60,8 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) 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 {
var se *os.SyscallError if se, ok := ne.Err.(*os.SyscallError); ok {
if errors.As(ne, &se) { if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
seStr := strings.ToLower(se.Error())
if strings.Contains(seStr, "broken pipe") ||
strings.Contains(seStr, "connection reset by peer") {
brokenPipe = true brokenPipe = true
} }
} }
@ -70,7 +70,12 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
stack := stack(3) stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false) httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n") headers := strings.Split(string(httpRequest), "\r\n")
maskAuthorization(headers) for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n") headersToStr := strings.Join(headers, "\r\n")
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, headersToStr, reset) logger.Printf("%s\n%s%s", err, headersToStr, reset)
@ -84,7 +89,7 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
if brokenPipe { if brokenPipe {
// If the connection is dead, we can't write a status to it. // If the connection is dead, we can't write a status to it.
c.Error(err.(error)) //nolint: errcheck c.Error(err.(error)) // nolint: errcheck
c.Abort() c.Abort()
} else { } else {
handle(c, err) handle(c, err)
@ -95,7 +100,7 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
} }
func defaultHandleRecovery(c *Context, _ any) { func defaultHandleRecovery(c *Context, err interface{}) {
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)
} }
@ -114,7 +119,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 := os.ReadFile(file) data, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
continue continue
} }
@ -126,51 +131,41 @@ func stack(skip int) []byte {
return buf.Bytes() return buf.Bytes()
} }
// maskAuthorization replaces any "Authorization: <token>" header with "Authorization: *", hiding sensitive credentials.
func maskAuthorization(headers []string) {
for idx, header := range headers {
key, _, _ := strings.Cut(header, ":")
if strings.EqualFold(key, "Authorization") {
headers[idx] = key + ": *"
}
}
}
// source returns a space-trimmed slice of the n'th line. // source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte { func source(lines [][]byte, n int) []byte {
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
if n < 0 || n >= len(lines) { if n < 0 || n >= len(lines) {
return dunnoBytes return dunno
} }
return bytes.TrimSpace(lines[n]) return bytes.TrimSpace(lines[n])
} }
// function returns, if possible, the name of the function containing the PC. // function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) string { func function(pc uintptr) []byte {
fn := runtime.FuncForPC(pc) fn := runtime.FuncForPC(pc)
if fn == nil { if fn == nil {
return dunno return dunno
} }
name := fn.Name() name := []byte(fn.Name())
// The name includes the path name to the package, which is unnecessary // The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots. // since the file name is already included. Plus, it has center dots.
// That is, we see // That is, we see
// runtime/debug.*T·ptrmethod // runtime/debug.*T·ptrmethod
// and want // and want
// *T.ptrmethod // *T.ptrmethod
// Also the package path might contain dot (e.g. code.google.com/...), // Also the package path might contains dot (e.g. code.google.com/...),
// so first eliminate the path prefix // so first eliminate the path prefix
if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 { if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
name = name[lastSlash+1:] name = name[lastSlash+1:]
} }
if period := strings.IndexByte(name, '.'); period >= 0 { if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:] name = name[period+1:]
} }
name = strings.ReplaceAll(name, "·", ".") name = bytes.Replace(name, centerDot, dot, -1)
return name return name
} }
// timeFormat returns a customized time string for logger.
func timeFormat(t time.Time) string { func timeFormat(t time.Time) string {
return t.Format("2006/01/02 - 15:04:05") timeString := t.Format("2006/01/02 - 15:04:05")
return timeString
} }

View File

@ -1,10 +1,12 @@
// 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"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -16,7 +18,7 @@ import (
) )
func TestPanicClean(t *testing.T) { func TestPanicClean(t *testing.T) {
buffer := new(strings.Builder) buffer := new(bytes.Buffer)
router := New() router := New()
password := "my-super-secret-password" password := "my-super-secret-password"
router.Use(RecoveryWithWriter(buffer)) router.Use(RecoveryWithWriter(buffer))
@ -25,14 +27,14 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery", w := performRequest(router, "GET", "/recovery",
header{ header{
Key: "Host", Key: "Host",
Value: "www.google.com", Value: "www.google.com",
}, },
header{ header{
Key: "Authorization", Key: "Authorization",
Value: "Bearer " + password, Value: fmt.Sprintf("Bearer %s", password),
}, },
header{ header{
Key: "Content-Type", Key: "Content-Type",
@ -48,14 +50,14 @@ 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(strings.Builder) buffer := new(bytes.Buffer)
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, http.MethodGet, "/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")
@ -66,7 +68,7 @@ func TestPanicInHandler(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/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")
@ -83,39 +85,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, http.MethodGet, "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
} }
func TestMaskAuthorization(t *testing.T) {
secret := "Bearer aaaabbbbccccddddeeeeffff"
headers := []string{
"Host: www.example.com",
"Authorization: " + secret,
"User-Agent: curl/7.51.0",
"Accept: */*",
"Content-Type: application/json",
"Content-Length: 1",
}
maskAuthorization(headers)
for _, h := range headers {
assert.NotContains(t, h, secret)
}
assert.Contains(t, headers, "Authorization: *")
}
func TestSource(t *testing.T) { func TestSource(t *testing.T) {
bs := source(nil, 0) bs := source(nil, 0)
assert.Equal(t, dunnoBytes, bs) assert.Equal(t, []byte("???"), 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, dunnoBytes, bs) assert.Equal(t, []byte("???"), bs)
bs = source(in, 1) bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs) assert.Equal(t, []byte("Hello world."), bs)
@ -123,7 +107,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, dunno, bs) assert.Equal(t, []byte("???"), bs)
} }
// TestPanicWithBrokenPipe asserts that recovery specifically handles // TestPanicWithBrokenPipe asserts that recovery specifically handles
@ -138,7 +122,8 @@ 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))
@ -152,7 +137,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e) panic(e)
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/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)
@ -161,10 +146,10 @@ func TestPanicWithBrokenPipe(t *testing.T) {
} }
func TestCustomRecoveryWithWriter(t *testing.T) { func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder) errBuffer := new(bytes.Buffer)
buffer := new(strings.Builder) buffer := new(bytes.Buffer)
router := New() router := New()
handleRecovery := func(c *Context, err any) { handleRecovery := func(c *Context, err interface{}) {
errBuffer.WriteString(err.(string)) errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)
} }
@ -173,7 +158,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -184,7 +169,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/recovery") w = performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -195,11 +180,11 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
} }
func TestCustomRecovery(t *testing.T) { func TestCustomRecovery(t *testing.T) {
errBuffer := new(strings.Builder) errBuffer := new(bytes.Buffer)
buffer := new(strings.Builder) buffer := new(bytes.Buffer)
router := New() router := New()
DefaultErrorWriter = buffer DefaultErrorWriter = buffer
handleRecovery := func(c *Context, err any) { handleRecovery := func(c *Context, err interface{}) {
errBuffer.WriteString(err.(string)) errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)
} }
@ -208,7 +193,7 @@ func TestCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -219,7 +204,7 @@ func TestCustomRecovery(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/recovery") w = performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -230,11 +215,11 @@ func TestCustomRecovery(t *testing.T) {
} }
func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
errBuffer := new(strings.Builder) errBuffer := new(bytes.Buffer)
buffer := new(strings.Builder) buffer := new(bytes.Buffer)
router := New() router := New()
DefaultErrorWriter = buffer DefaultErrorWriter = buffer
handleRecovery := func(c *Context, err any) { handleRecovery := func(c *Context, err interface{}) {
errBuffer.WriteString(err.(string)) errBuffer.WriteString(err.(string))
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)
} }
@ -243,7 +228,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, http.MethodGet, "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -254,7 +239,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, http.MethodGet, "/recovery") w = performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")

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.
@ -7,8 +7,6 @@ package render
import ( import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/fs"
) )
// Delims represents a set of Left and Right delimiters for HTML template rendering. // Delims represents a set of Left and Right delimiters for HTML template rendering.
@ -22,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, any) Render Instance(string, interface{}) Render
} }
// HTMLProduction contains template reference and its delims. // HTMLProduction contains template reference and its delims.
@ -33,25 +31,23 @@ type HTMLProduction struct {
// HTMLDebug contains template delims and pattern and function with file list. // HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct { type HTMLDebug struct {
Files []string Files []string
Glob string Glob string
FileSystem http.FileSystem Delims Delims
Patterns []string FuncMap template.FuncMap
Delims Delims
FuncMap template.FuncMap
} }
// HTML contains template reference and its name with given interface object. // HTML contains template reference and its name with given interface object.
type HTML struct { type HTML struct {
Template *template.Template Template *template.Template
Name string Name string
Data any Data interface{}
} }
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 any) Render { func (r HTMLProduction) Instance(name string, data interface{}) Render {
return HTML{ return HTML{
Template: r.Template, Template: r.Template,
Name: name, Name: name,
@ -60,14 +56,13 @@ func (r HTMLProduction) Instance(name string, data any) 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 any) Render { func (r HTMLDebug) Instance(name string, data interface{}) Render {
return HTML{ return HTML{
Template: r.loadTemplate(), Template: r.loadTemplate(),
Name: name, Name: name,
Data: data, Data: data,
} }
} }
func (r HTMLDebug) loadTemplate() *template.Template { func (r HTMLDebug) loadTemplate() *template.Template {
if r.FuncMap == nil { if r.FuncMap == nil {
r.FuncMap = template.FuncMap{} r.FuncMap = template.FuncMap{}
@ -78,11 +73,7 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if r.Glob != "" { if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob)) return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
} }
if r.FileSystem != nil && len(r.Patterns) > 0 { panic("the HTML debug render was created without files or glob pattern")
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
}
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
} }
// Render (HTML) executes template and writes its result with custom ContentType for response. // Render (HTML) executes template and writes its result with custom ContentType for response.

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,53 +9,53 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"unicode"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
"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 any Data interface{}
} }
// IndentedJSON contains the given interface object. // IndentedJSON contains the given interface object.
type IndentedJSON struct { type IndentedJSON struct {
Data any Data interface{}
} }
// 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 any Data interface{}
} }
// 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 any Data interface{}
} }
// AsciiJSON contains the given interface object. // AsciiJSON contains the given interface object.
type AsciiJSON struct { type AsciiJSON struct {
Data any Data interface{}
} }
// PureJSON contains the given interface object. // PureJSON contains the given interface object.
type PureJSON struct { type PureJSON struct {
Data any Data interface{}
} }
var ( var jsonContentType = []string{"application/json; charset=utf-8"}
jsonContentType = []string{"application/json; charset=utf-8"} var jsonpContentType = []string{"application/javascript; charset=utf-8"}
jsonpContentType = []string{"application/javascript; charset=utf-8"} var jsonAsciiContentType = []string{"application/json"}
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) error { func (r JSON) Render(w http.ResponseWriter) (err error) {
return WriteJSON(w, r.Data) if err = WriteJSON(w, r.Data); err != nil {
panic(err)
}
return
} }
// WriteContentType (JSON) writes JSON ContentType. // WriteContentType (JSON) writes JSON ContentType.
@ -64,9 +64,9 @@ 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 any) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
jsonBytes, err := json.API.Marshal(obj) jsonBytes, err := json.Marshal(obj)
if err != nil { if err != nil {
return err return err
} }
@ -77,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType. // Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
func (r IndentedJSON) Render(w http.ResponseWriter) error { func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ") jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil { if err != nil {
return err return err
} }
@ -93,14 +93,15 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType. // Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error { func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.API.Marshal(r.Data) jsonBytes, err := json.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes, if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
bytesconv.StringToBytes("]")) { bytesconv.StringToBytes("]")) {
if _, err = w.Write(bytesconv.StringToBytes(r.Prefix)); err != nil { _, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil {
return err return err
} }
} }
@ -116,7 +117,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType. // Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) { func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
ret, err := json.API.Marshal(r.Data) ret, err := json.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
@ -127,19 +128,20 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
} }
callback := template.JSEscapeString(r.Callback) callback := template.JSEscapeString(r.Callback)
if _, err = w.Write(bytesconv.StringToBytes(callback)); err != nil { _, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil {
return err return err
} }
_, err = w.Write(bytesconv.StringToBytes("("))
if _, err = w.Write(bytesconv.StringToBytes("(")); err != nil { if err != nil {
return err return err
} }
_, err = w.Write(ret)
if _, err = w.Write(ret); err != nil { if err != nil {
return err return err
} }
_, err = w.Write(bytesconv.StringToBytes(");"))
if _, err = w.Write(bytesconv.StringToBytes(");")); err != nil { if err != nil {
return err return err
} }
@ -152,23 +154,20 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
} }
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType. // Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) error { func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
ret, err := json.API.Marshal(r.Data) ret, err := json.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
var buffer bytes.Buffer var buffer bytes.Buffer
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
for _, r := range bytesconv.BytesToString(ret) { for _, r := range bytesconv.BytesToString(ret) {
if r > unicode.MaxASCII { cvt := string(r)
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf if r >= 128 {
buffer.Write(escapeBuf) cvt = fmt.Sprintf("\\u%04x", int64(r))
} else {
buffer.WriteByte(byte(r))
} }
buffer.WriteString(cvt)
} }
_, err = w.Write(buffer.Bytes()) _, err = w.Write(buffer.Bytes())
@ -177,13 +176,13 @@ func (r AsciiJSON) Render(w http.ResponseWriter) 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.
func (r PureJSON) Render(w http.ResponseWriter) error { func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
encoder := json.API.NewEncoder(w) encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false) encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data) return encoder.Encode(r.Data)
} }

View File

@ -1,8 +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 //go:build !nomsgpack
// +build !nomsgpack
package render package render
@ -12,15 +13,13 @@ 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 ( var (
_ Render = MsgPack{} _ Render = MsgPack{}
) )
// MsgPack contains the given interface object. // MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data any Data interface{}
} }
var msgpackContentType = []string{"application/msgpack; charset=utf-8"} var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
@ -36,7 +35,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 any) error { func WriteMsgPack(w http.ResponseWriter, obj interface{}) 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"
"google.golang.org/protobuf/proto" "github.com/golang/protobuf/proto"
) )
// ProtoBuf contains the given interface object. // ProtoBuf contains the given interface object.
type ProtoBuf struct { type ProtoBuf struct {
Data any Data interface{}
} }
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.

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.

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,21 @@ type Render interface {
} }
var ( var (
_ Render = (*JSON)(nil) _ Render = JSON{}
_ Render = (*IndentedJSON)(nil) _ Render = IndentedJSON{}
_ Render = (*SecureJSON)(nil) _ Render = SecureJSON{}
_ Render = (*JsonpJSON)(nil) _ Render = JsonpJSON{}
_ Render = (*XML)(nil) _ Render = XML{}
_ Render = (*String)(nil) _ Render = String{}
_ Render = (*Redirect)(nil) _ Render = Redirect{}
_ Render = (*Data)(nil) _ Render = Data{}
_ Render = (*HTML)(nil) _ Render = HTML{}
_ HTMLRender = (*HTMLDebug)(nil) _ HTMLRender = HTMLDebug{}
_ HTMLRender = (*HTMLProduction)(nil) _ HTMLRender = HTMLProduction{}
_ Render = (*YAML)(nil) _ Render = YAML{}
_ 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

@ -1,8 +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 //go:build !nomsgpack
// +build !nomsgpack
package render package render
@ -12,7 +13,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -21,7 +21,7 @@ import (
func TestRenderMsgPack(t *testing.T) { func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
} }
@ -30,7 +30,7 @@ func TestRenderMsgPack(t *testing.T) {
err := (MsgPack{data}).Render(w) err := (MsgPack{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
h := new(codec.MsgpackHandle) h := new(codec.MsgpackHandle)
assert.NotNil(t, h) assert.NotNil(t, h)
@ -38,7 +38,7 @@ func TestRenderMsgPack(t *testing.T) {
assert.NotNil(t, buf) assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data) err = codec.NewEncoder(buf, h).Encode(data)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String()) assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
} }

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,18 +8,16 @@ import (
"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/gin-gonic/gin/codec/json" "github.com/golang/protobuf/proto"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto" testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
// TODO unit tests // TODO unit tests
@ -27,7 +25,7 @@ import (
func TestRenderJSON(t *testing.T) { func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
"html": "<b>", "html": "<b>",
} }
@ -37,30 +35,30 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
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 TestRenderJSONError(t *testing.T) { func TestRenderJSONPanics(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
require.Error(t, (JSON{data}).Render(w)) assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
} }
func TestRenderIndentedJSON(t *testing.T) { func TestRenderIndentedJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
"bar": "foo", "bar": "foo",
} }
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String()) assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
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"))
} }
@ -70,12 +68,12 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderSecureJSON(t *testing.T) { func TestRenderSecureJSON(t *testing.T) {
w1 := httptest.NewRecorder() w1 := httptest.NewRecorder()
data := map[string]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
} }
@ -84,19 +82,19 @@ func TestRenderSecureJSON(t *testing.T) {
err1 := (SecureJSON{"while(1);", data}).Render(w1) err1 := (SecureJSON{"while(1);", data}).Render(w1)
require.NoError(t, err1) assert.NoError(t, err1)
assert.JSONEq(t, "{\"foo\":\"bar\"}", w1.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
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]any{{ datas := []map[string]interface{}{{
"foo": "bar", "foo": "bar",
}, { }, {
"bar": "foo", "bar": "foo",
}} }}
err2 := (SecureJSON{"while(1);", datas}).Render(w2) err2 := (SecureJSON{"while(1);", datas}).Render(w2)
require.NoError(t, err2) assert.NoError(t, err2)
assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String()) assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type"))
} }
@ -107,12 +105,12 @@ func TestRenderSecureJSONFail(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (SecureJSON{"while(1);", data}).Render(w) err := (SecureJSON{"while(1);", data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderJsonpJSON(t *testing.T) { func TestRenderJsonpJSON(t *testing.T) {
w1 := httptest.NewRecorder() w1 := httptest.NewRecorder()
data := map[string]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
} }
@ -121,80 +119,35 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1) err1 := (JsonpJSON{"x", data}).Render(w1)
require.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]any{{ datas := []map[string]interface{}{{
"foo": "bar", "foo": "bar",
}, { }, {
"bar": "foo", "bar": "foo",
}} }}
err2 := (JsonpJSON{"x", datas}).Render(w2) err2 := (JsonpJSON{"x", datas}).Render(w2)
require.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.API.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]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
} }
(JsonpJSON{"", data}).WriteContentType(w) (JsonpJSON{"", data}).WriteContentType(w)
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
e := (JsonpJSON{"", data}).Render(w) e := (JsonpJSON{"", data}).Render(w)
require.NoError(t, e) assert.NoError(t, e)
assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -204,27 +157,27 @@ func TestRenderJsonpJSONFail(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (JsonpJSON{"x", data}).Render(w) err := (JsonpJSON{"x", data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderAsciiJSON(t *testing.T) { func TestRenderAsciiJSON(t *testing.T) {
w1 := httptest.NewRecorder() w1 := httptest.NewRecorder()
data1 := map[string]any{ data1 := map[string]interface{}{
"lang": "GO语言", "lang": "GO语言",
"tag": "<br>", "tag": "<br>",
} }
err := (AsciiJSON{data1}).Render(w1) err := (AsciiJSON{data1}).Render(w1)
require.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String()) assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
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 := 3.1415926 data2 := float64(3.1415926)
err = (AsciiJSON{data2}).Render(w2) err = (AsciiJSON{data2}).Render(w2)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "3.1415926", w2.Body.String()) assert.Equal(t, "3.1415926", w2.Body.String())
} }
@ -233,22 +186,22 @@ func TestRenderAsciiJSONFail(t *testing.T) {
data := make(chan int) data := make(chan int)
// json: unsupported type: chan int // json: unsupported type: chan int
require.Error(t, (AsciiJSON{data}).Render(w)) assert.Error(t, (AsciiJSON{data}).Render(w))
} }
func TestRenderPureJSON(t *testing.T) { func TestRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]any{ data := map[string]interface{}{
"foo": "bar", "foo": "bar",
"html": "<b>", "html": "<b>",
} }
err := (PureJSON{data}).Render(w) err := (PureJSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String()) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
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]any type xmlmap map[string]interface{}
// 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 {
@ -281,53 +234,25 @@ b:
d: [3, 4] d: [3, 4]
` `
(YAML{data}).WriteContentType(w) (YAML{data}).WriteContentType(w)
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w) err := (YAML{data}).Render(w)
require.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())
// With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3 assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
// We're checking that the output contains the expected data, not the exact formatting
output := w.Body.String()
assert.Contains(t, output, "a : Easy!")
assert.Contains(t, output, "b:")
assert.Contains(t, output, "c: 2")
assert.Contains(t, output, "d: [3, 4]")
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() (any, error) { func (ft *fail) MarshalYAML() (interface{}, error) {
return nil, errors.New("fail") return nil, errors.New("fail")
} }
func TestRenderYAMLFail(t *testing.T) { func TestRenderYAMLFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err := (YAML{&fail{}}).Render(w) err := (YAML{&fail{}}).Render(w)
require.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)
require.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)
require.Error(t, err)
} }
// test Protobuf rendering // test Protobuf rendering
@ -342,12 +267,12 @@ func TestRenderProtoBuf(t *testing.T) {
(ProtoBuf{data}).WriteContentType(w) (ProtoBuf{data}).WriteContentType(w)
protoData, err := proto.Marshal(data) protoData, err := proto.Marshal(data)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
err = (ProtoBuf{data}).Render(w) err = (ProtoBuf{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, string(protoData), w.Body.String()) assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
} }
@ -356,7 +281,7 @@ func TestRenderProtoBufFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := &testdata.Test{} data := &testdata.Test{}
err := (ProtoBuf{data}).Render(w) err := (ProtoBuf{data}).Render(w)
require.Error(t, err) assert.Error(t, err)
} }
func TestRenderXML(t *testing.T) { func TestRenderXML(t *testing.T) {
@ -370,14 +295,14 @@ func TestRenderXML(t *testing.T) {
err := (XML{data}).Render(w) err := (XML{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String()) assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderRedirect(t *testing.T) { func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil) req, err := http.NewRequest("GET", "/test-redirect", nil)
require.NoError(t, err) assert.NoError(t, err)
data1 := Redirect{ data1 := Redirect{
Code: http.StatusMovedPermanently, Code: http.StatusMovedPermanently,
@ -387,7 +312,7 @@ func TestRenderRedirect(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err = data1.Render(w) err = data1.Render(w)
require.NoError(t, err) assert.NoError(t, err)
data2 := Redirect{ data2 := Redirect{
Code: http.StatusOK, Code: http.StatusOK,
@ -398,7 +323,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() { assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w) err := data2.Render(w)
require.NoError(t, err) assert.NoError(t, err)
}) })
data3 := Redirect{ data3 := Redirect{
@ -409,7 +334,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder() w = httptest.NewRecorder()
err = data3.Render(w) err = data3.Render(w)
require.NoError(t, err) assert.NoError(t, err)
// only improve coverage // only improve coverage
data2.WriteContentType(w) data2.WriteContentType(w)
@ -424,7 +349,7 @@ func TestRenderData(t *testing.T) {
Data: data, Data: data,
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String()) assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
} }
@ -434,16 +359,16 @@ func TestRenderString(t *testing.T) {
(String{ (String{
Format: "hello %s %d", Format: "hello %s %d",
Data: []any{}, Data: []interface{}{},
}).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: []any{"manu", 2}, Data: []interface{}{"manu", 2},
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "hola manu 2", w.Body.String()) assert.Equal(t, "hola manu 2", w.Body.String())
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"))
} }
@ -453,10 +378,10 @@ func TestRenderStringLenZero(t *testing.T) {
err := (String{ err := (String{
Format: "hola %s %d", Format: "hola %s %d",
Data: []any{}, Data: []interface{}{},
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "hola %s %d", w.Body.String()) assert.Equal(t, "hola %s %d", w.Body.String())
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"))
} }
@ -466,13 +391,13 @@ 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]any{ instance := htmlRender.Instance("t", map[string]interface{}{
"name": "alexandernyquist", "name": "alexandernyquist",
}) })
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -482,88 +407,58 @@ 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]any{ instance := htmlRender.Instance("", map[string]interface{}{
"name": "alexandernyquist", "name": "alexandernyquist",
}) })
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderHTMLDebugFiles(t *testing.T) { func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
htmlRender := HTMLDebug{ htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
Files: []string{"../testdata/template/hello.tmpl"}, Glob: "",
Glob: "", Delims: Delims{Left: "{[{", Right: "}]}"},
FileSystem: nil, FuncMap: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
} }
instance := htmlRender.Instance("hello.tmpl", map[string]any{ instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
"name": "thinkerou", "name": "thinkerou",
}) })
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String()) assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderHTMLDebugGlob(t *testing.T) { func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
htmlRender := HTMLDebug{ htmlRender := HTMLDebug{Files: nil,
Files: nil, Glob: "../testdata/template/hello*",
Glob: "../testdata/template/hello*", Delims: Delims{Left: "{[{", Right: "}]}"},
FileSystem: nil, FuncMap: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
} }
instance := htmlRender.Instance("hello.tmpl", map[string]any{ instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
"name": "thinkerou", "name": "thinkerou",
}) })
err := instance.Render(w) err := instance.Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugFS(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: http.Dir("../testdata/template"),
Patterns: []string{"hello.tmpl"},
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
err := instance.Render(w)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String()) assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderHTMLDebugPanics(t *testing.T) { func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{ htmlRender := HTMLDebug{Files: nil,
Files: nil, Glob: "",
Glob: "", Delims: Delims{"{{", "}}"},
FileSystem: nil, FuncMap: nil,
Patterns: nil,
Delims: Delims{"{{", "}}"},
FuncMap: nil,
} }
assert.Panics(t, func() { htmlRender.Instance("", nil) }) assert.Panics(t, func() { htmlRender.Instance("", nil) })
} }
@ -583,7 +478,7 @@ func TestRenderReader(t *testing.T) {
Headers: headers, Headers: headers,
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length")) assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
@ -606,23 +501,10 @@ func TestRenderReaderNoContentLength(t *testing.T) {
Headers: headers, Headers: headers,
}).Render(w) }).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header()) assert.NotContains(t, "Content-Length", w.Header())
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 TestRenderWriteError(t *testing.T) {
data := []any{"value1", "value2"}
prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{
bufString: prefix,
ResponseRecorder: httptest.NewRecorder(),
}
err := r.Render(ew)
require.Error(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.
@ -14,7 +14,7 @@ import (
// 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 []any Data []interface{}
} }
var plainContentType = []string{"text/plain; charset=utf-8"} var plainContentType = []string{"text/plain; charset=utf-8"}
@ -30,7 +30,7 @@ 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 []any) (err error) { func WriteString(w http.ResponseWriter, format string, data []interface{}) (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...)

View File

@ -1,36 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
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 any Data interface{}
} }
var xmlContentType = []string{"application/xml; charset=utf-8"} var xmlContentType = []string{"application/xml; charset=utf-8"}

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