Compare commits

..

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

87 changed files with 3806 additions and 6587 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

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

View File

@ -8,40 +8,28 @@ on:
branches: branches:
- master - master
permissions:
contents: read
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Setup go
uses: actions/checkout@v4 uses: actions/setup-go@v3
with: with:
fetch-depth: 0 go-version: '^1.16'
- name: Set up Go - name: Checkout repository
uses: actions/setup-go@v5 uses: actions/checkout@v3
with:
go-version: "^1"
- name: Setup golangci-lint - name: Setup golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v3.2.0
with: with:
version: v1.61.0 version: v1.45.0
args: --verbose args: --verbose
test: test:
needs: lint needs: lint
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
go: ["1.23", "1.24"] go: [1.14, 1.15, 1.16, 1.17, 1.18]
test-tags: test-tags: ['', nomsgpack]
[
"",
"-tags nomsgpack",
'--ldflags="-checklinkname=0" -tags "sonic avx"',
"-tags go_json",
"-race",
]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
go-build: ~/.cache/go-build go-build: ~/.cache/go-build
@ -55,17 +43,16 @@ jobs:
GOPROXY: https://proxy.golang.org GOPROXY: https://proxy.golang.org
steps: steps:
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: | path: |
${{ matrix.go-build }} ${{ matrix.go-build }}
@ -78,6 +65,20 @@ jobs:
run: make test run: make test
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v3
with: with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }} flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
notification-gitter:
needs: test
runs-on: ubuntu-latest
steps:
- name: Notification failure message
if: failure()
run: |
PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" -d level=error https://webhooks.gitter.im/e/7f95bf605c4d356372f4
- name: Notification success message
if: success()
run: |
PR_OR_COMPARE="$(if [ "${{ github.event.pull_request }}" != "" ]; then echo "${{ github.event.pull_request.html_url }}"; else echo "${{ github.event.compare }}"; fi)"
curl -d message="GitHub Actions [$GITHUB_REPOSITORY]($PR_OR_COMPARE) ($GITHUB_REF) [normal]($GITHUB_API_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) ($GITHUB_RUN_NUMBER)" https://webhooks.gitter.im/e/7f95bf605c4d356372f4

View File

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

@ -3,11 +3,12 @@ run:
linters: linters:
enable: enable:
- asciicheck - asciicheck
- depguard
- dogsled - dogsled
- durationcheck - durationcheck
- errcheck - errcheck
- errorlint - errorlint
- copyloopvar - exportloopref
- gci - gci
- gofmt - gofmt
- goimports - goimports
@ -16,35 +17,8 @@ linters:
- nakedret - nakedret
- nilerr - nilerr
- nolintlint - nolintlint
- perfsprint
- revive - revive
- testifylint
- usestdlibvars
- wastedassign - wastedassign
linters-settings:
gosec:
# To select a subset of rules to run.
# Available rules: https://github.com/securego/gosec#available-rules
# Default: [] - means include all rules
includes:
- G102
- G106
- G108
- G109
- G111
- G112
- G201
- G203
perfsprint:
err-error: true
errorf: true
int-conversion: true
sprintf1: true
strconcat: true
testifylint:
enable-all: true
issues: issues:
exclude-rules: exclude-rules:
- linters: - linters:
@ -63,9 +37,3 @@ issues:
- path: _test\.go - path: _test\.go
linters: linters:
- gosec # security is not make sense in tests - gosec # security is not make sense in tests
- linters:
- revive
path: _test\.go
- path: gin.go
linters:
- gci

View File

@ -1,7 +1,8 @@
project_name: gin project_name: gin
builds: builds:
- # If true, skip the build. -
# If true, skip the build.
# Useful for library projects. # Useful for library projects.
# Default is false # Default is false
skip: true skip: true
@ -9,7 +10,7 @@ builds:
changelog: changelog:
# Set it to true if you wish to skip the changelog generation. # Set it to true if you wish to skip the changelog generation.
# This may result in an empty release notes on GitHub/GitLab/Gitea. # This may result in an empty release notes on GitHub/GitLab/Gitea.
disable: false skip: false
# Changelog generation implementation to use. # Changelog generation implementation to use.
# #
@ -20,7 +21,7 @@ changelog:
# - `github-native`: uses the GitHub release notes generation API, disables the groups feature. # - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
# #
# Defaults to `git`. # Defaults to `git`.
use: github use: git
# Sorts the changelog by the commit's messages. # Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty # Could either be asc, desc or empty
@ -37,20 +38,20 @@ changelog:
- title: Features - title: Features
regexp: "^.*feat[(\\w)]*:+.*$" regexp: "^.*feat[(\\w)]*:+.*$"
order: 0 order: 0
- title: "Bug fixes" - title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$" regexp: "^.*fix[(\\w)]*:+.*$"
order: 1 order: 1
- title: "Enhancements" - title: 'Enhancements'
regexp: "^.*chore[(\\w)]*:+.*$" regexp: "^.*chore[(\\w)]*:+.*$"
order: 2 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 - title: Others
order: 999 order: 999
filters:
# Commit messages matching the regexp listed here will be removed from
# the changelog
# Default is empty
exclude:
- '^docs'
- 'CICD'
- typo

View File

@ -1,147 +1,5 @@
# 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 ## Gin v1.8.1
### ENHANCEMENTS ### ENHANCEMENTS
@ -150,12 +8,12 @@
## Gin v1.8.0 ## Gin v1.8.0
### BREAK CHANGES ## Break Changes
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP` * 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) * gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
### BUG FIXES ### BUGFIXES
* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861) * 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: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
@ -192,7 +50,7 @@
## Gin v1.7.7 ## Gin v1.7.7
### BUG FIXES ### BUGFIXES
* 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). * 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: 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).
@ -210,37 +68,37 @@
## Gin v1.7.6 ## Gin v1.7.6
### BUG FIXES ### BUGFIXES
* bump new release to fix v1.7.5 release error by using v1.7.4 codes. * bump new release to fix v1.7.5 release error by using v1.7.4 codes.
## Gin v1.7.4 ## Gin v1.7.4
### BUG FIXES ### BUGFIXES
* bump new release to fix checksum mismatch * bump new release to fix checksum mismatch
## Gin v1.7.3 ## Gin v1.7.3
### BUG FIXES ### BUGFIXES
* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796) * 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 ## Gin v1.7.2
### BUG FIXES ### BUGFIXES
* 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). * 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 ## Gin v1.7.1
### BUG FIXES ### BUGFIXES
* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675)) * 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 +113,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 +137,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 +178,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 +191,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 +331,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,11 +8,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 +30,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,37 +42,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: misspell
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: tools .PHONY: tools
# Install tools (golint and misspell).
tools: tools:
@if [ $(GO_VERSION) -gt 15 ]; then \ @if [ $(GO_VERSION) -gt 15 ]; then \
$(GO) install golang.org/x/lint/golint@latest; \ $(GO) install golang.org/x/lint/golint@latest; \
@ -84,23 +75,3 @@ tools:
$(GO) install golang.org/x/lint/golint; \ $(GO) install golang.org/x/lint/golint; \
$(GO) install github.com/client9/misspell/cmd/misspell; \ $(GO) install github.com/client9/misspell/cmd/misspell; \
fi fi
.PHONY: help
# Help.
help:
@echo ''
@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

2326
README.md

File diff suppressed because it is too large Load Diff

10
any.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.18
// +build !go1.18
package gin
type any = interface{}

25
auth.go
View File

@ -16,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
@ -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

@ -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

@ -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 {

10
binding/any.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.18
// +build !go1.18
package binding
type any = interface{}

View File

@ -3,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
@ -21,7 +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" MIMETOML = "application/toml"
) )
@ -73,19 +73,18 @@ 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 = tomlBinding{}
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,7 +103,7 @@ 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: case MIMETOML:
return TOML return TOML

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,7 +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" MIMETOML = "application/toml"
) )
@ -81,7 +81,6 @@ var (
Uri = uriBinding{} Uri = uriBinding{}
Header = headerBinding{} Header = headerBinding{}
TOML = tomlBinding{} 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,7 +97,7 @@ 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

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strconv"
"strings" "strings"
"sync" "sync"
@ -22,23 +22,28 @@ type SliceValidationError []error
// Error concatenates all error elements in SliceValidationError into a single string separated by \n. // Error concatenates all error elements in SliceValidationError into a single string separated by \n.
func (err SliceValidationError) Error() string { func (err SliceValidationError) Error() string {
if len(err) == 0 { n := len(err)
switch n {
case 0:
return "" return ""
} default:
var b strings.Builder var b strings.Builder
for i := 0; i < len(err); i++ { if err[0] != nil {
if err[i] != nil { fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
if b.Len() > 0 { }
b.WriteString("\n") if n > 1 {
for i := 1; i < n; i++ {
if err[i] != nil {
b.WriteString("\n")
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
} }
b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
} }
} }
return b.String() return b.String()
}
} }
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 any) error {
@ -49,10 +54,7 @@ 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:

View File

@ -12,15 +12,11 @@ import (
func BenchmarkSliceValidationError(b *testing.B) { func BenchmarkSliceValidationError(b *testing.B) {
const size int = 100 const size int = 100
for i := 0; i < b.N; i++ {
e := make(SliceValidationError, size) e := make(SliceValidationError, size)
for j := 0; j < size; j++ { for j := 0; j < size; j++ {
e[j] = errors.New(strconv.Itoa(j)) e[j] = errors.New(strconv.Itoa(j))
} }
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if len(e.Error()) == 0 { if len(e.Error()) == 0 {
b.Errorf("error") b.Errorf("error")
} }

View File

@ -7,7 +7,6 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -20,7 +19,7 @@ import (
var ( var (
errUnknownType = errors.New("unknown type") errUnknownType = errors.New("unknown type")
// ErrConvertMapStringSlice can not convert to map[string][]string // ErrConvertMapStringSlice can not covert to map[string][]string
ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings") ErrConvertMapStringSlice = errors.New("can not convert to map slices of strings")
// ErrConvertToMapString can not convert to map[string]string // ErrConvertToMapString can not convert to map[string]string
@ -159,69 +158,12 @@ 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.
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) { 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 {
@ -232,46 +174,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 +192,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)
} }
@ -330,17 +235,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.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.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,24 +296,18 @@ 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
} }
@ -477,8 +369,11 @@ 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 any, form map[string][]string) error {

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) {
@ -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)
} }
@ -141,8 +132,8 @@ func TestMappingURI(t *testing.T) {
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,26 +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) { func TestMapFormWithTag(t *testing.T) {
@ -177,8 +150,8 @@ func TestMapFormWithTag(t *testing.T) {
F int `externalTag:"field"` F int `externalTag:"field"`
} }
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag") err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, int(6), s.F)
} }
func TestMappingTime(t *testing.T) { func TestMappingTime(t *testing.T) {
@ -192,7 +165,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 +174,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 +189,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 +206,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 +221,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 +241,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 +265,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 +286,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.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(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.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(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.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(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.EqualValues(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.EqualValues(t, expected, s.FileData)
} }

View File

@ -15,7 +15,7 @@ import (
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON // EnableDecoderUseNumber is used to call the UseNumber method on the JSON
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an // Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
// 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

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

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

View 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))
@ -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
@ -128,12 +127,12 @@ func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file test
assert.Equal(t, int64(len(file.Content)), fh.Size) assert.Equal(t, int64(len(file.Content)), fh.Size)
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 interface{}) 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

@ -6,7 +6,7 @@ package binding
import ( import (
"errors" "errors"
"io" "io/ioutil"
"net/http" "net/http"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -19,7 +19,7 @@ func (protobufBinding) Name() string {
} }
func (b protobufBinding) Bind(req *http.Request, obj any) error { func (b protobufBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body) buf, err := ioutil.ReadAll(req.Body)
if err != nil { if err != nil {
return err return err
} }

View File

@ -18,6 +18,14 @@ func (tomlBinding) Name() string {
return "toml" return "toml"
} }
func decodeToml(r io.Reader, obj any) error {
decoder := toml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return decoder.Decode(obj)
}
func (tomlBinding) Bind(req *http.Request, obj any) error { func (tomlBinding) Bind(req *http.Request, obj any) error {
return decodeToml(req.Body, obj) return decodeToml(req.Body, obj)
} }
@ -25,11 +33,3 @@ func (tomlBinding) Bind(req *http.Request, obj any) error {
func (tomlBinding) BindBody(body []byte, obj any) error { func (tomlBinding) BindBody(body []byte, obj any) error {
return decodeToml(bytes.NewReader(body), obj) 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

@ -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 {
@ -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)
} }
@ -164,8 +163,8 @@ func TestValidateNoValidationPointers(t *testing.T) {
//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)
} }
@ -174,49 +173,25 @@ type Object map[string]any
func TestValidatePrimitives(t *testing.T) { func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1} obj := Object{"foo": "bar", "bar": 1}
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) {
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
s.Integer = 0
}
func TestValidateAndModifyStruct(t *testing.T) {
// This validates that pointers to structs are passed to the validator
// giving us the ability to modify the struct being validated.
engine, ok := Validator.Engine().(*validator.Validate)
assert.True(t, ok)
engine.RegisterStructValidation(toZero, structModifyValidation{})
s := structModifyValidation{Integer: 1}
errs := validate(&s)
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

@ -9,7 +9,7 @@ import (
"io" "io"
"net/http" "net/http"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v2"
) )
type yamlBinding struct{} type yamlBinding struct{}

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%

View File

@ -7,7 +7,7 @@ package gin
import ( import (
"errors" "errors"
"io" "io"
"io/fs" "io/ioutil"
"log" "log"
"math" "math"
"mime/multipart" "mime/multipart"
@ -15,7 +15,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -35,7 +34,6 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML MIMETOML = binding.MIMETOML
) )
@ -45,10 +43,6 @@ const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
// ContextKey is the key that a Context returns itself for. // ContextKey is the key that a Context returns itself for.
const ContextKey = "_gin-gonic/gin/contextkey" const ContextKey = "_gin-gonic/gin/contextkey"
type ContextKeyType int
const ContextRequestKey ContextKeyType = 0
// abortIndex represents a typical value used in abort functions. // abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 const abortIndex int8 = math.MaxInt8 >> 1
@ -119,27 +113,20 @@ func (c *Context) Copy() *Context {
cp := Context{ cp := Context{
writermem: c.writermem, writermem: c.writermem,
Request: c.Request, Request: c.Request,
Params: c.Params,
engine: c.engine, engine: c.engine,
} }
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = abortIndex cp.index = abortIndex
cp.handlers = nil cp.handlers = nil
cp.fullPath = c.fullPath cp.Keys = map[string]any{}
for k, v := range c.Keys {
cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
c.mu.RLock()
for k, v := range cKeys {
cp.Keys[k] = v cp.Keys[k] = v
} }
c.mu.RUnlock() paramCopy := make([]Param, len(cp.Params))
copy(paramCopy, cp.Params)
cParams := c.Params cp.Params = paramCopy
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
return &cp return &cp
} }
@ -154,9 +141,6 @@ func (c *Context) HandlerName() string {
func (c *Context) HandlerNames() []string { func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers)) hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers { for _, val := range c.handlers {
if val == nil {
continue
}
hn = append(hn, nameOfFunction(val)) hn = append(hn, nameOfFunction(val))
} }
return hn return hn
@ -169,7 +153,6 @@ func (c *Context) Handler() HandlerFunc {
// FullPath returns a matched route full path. For not found routes // FullPath returns a matched route full path. For not found routes
// returns an empty string. // returns an empty string.
//
// router.GET("/user/:id", func(c *gin.Context) { // router.GET("/user/:id", func(c *gin.Context) {
// c.FullPath() == "/user/:id" // true // c.FullPath() == "/user/:id" // true
// }) // })
@ -187,9 +170,7 @@ func (c *Context) FullPath() string {
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
for c.index < int8(len(c.handlers)) { for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] != nil {
c.handlers[c.index](c) c.handlers[c.index](c)
}
c.index++ c.index++
} }
} }
@ -266,20 +247,20 @@ func (c *Context) Error(err error) *Error {
// It also lazy initializes c.Keys if it was not used previously. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) { func (c *Context) Set(key string, value any) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[string]any) c.Keys = make(map[string]any)
} }
c.Keys[key] = value c.Keys[key] = value
c.mu.Unlock()
} }
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false) // If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) { func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key] value, exists = c.Keys[key]
c.mu.RUnlock()
return return
} }
@ -291,171 +272,108 @@ func (c *Context) MustGet(key string) any {
panic("Key \"" + key + "\" does not exist") panic("Key \"" + key + "\" does not exist")
} }
func getTyped[T any](c *Context, key string) (res T) { // GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
if val, ok := c.Get(key); ok && val != nil { if val, ok := c.Get(key); ok && val != nil {
res, _ = val.(T) s, _ = val.(string)
} }
return return
} }
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
return getTyped[string](c, key)
}
// GetBool returns the value associated with the key as a boolean. // GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key string) (b bool) { func (c *Context) GetBool(key string) (b bool) {
return getTyped[bool](c, key) if val, ok := c.Get(key); ok && val != nil {
b, _ = val.(bool)
}
return
} }
// GetInt returns the value associated with the key as an integer. // GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key string) (i int) { func (c *Context) GetInt(key string) (i int) {
return getTyped[int](c, key) if val, ok := c.Get(key); ok && val != nil {
i, _ = val.(int)
}
return
} }
// GetInt8 returns the value associated with the key as an integer 8. // GetInt64 returns the value associated with the key as an integer.
func (c *Context) GetInt8(key string) (i8 int8) {
return getTyped[int8](c, key)
}
// GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key string) (i16 int16) {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key string) (i32 int32) {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key string) (i64 int64) { func (c *Context) GetInt64(key string) (i64 int64) {
return getTyped[int64](c, key) if val, ok := c.Get(key); ok && val != nil {
i64, _ = val.(int64)
}
return
} }
// GetUint returns the value associated with the key as an unsigned integer. // GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) { func (c *Context) GetUint(key string) (ui uint) {
return getTyped[uint](c, key) if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
} }
// GetUint8 returns the value associated with the key as an unsigned integer 8. // GetUint64 returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint8(key string) (ui8 uint8) {
return getTyped[uint8](c, key)
}
// GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key string) (ui16 uint16) {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key string) (ui32 uint32) {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key string) (ui64 uint64) { func (c *Context) GetUint64(key string) (ui64 uint64) {
return getTyped[uint64](c, key) if val, ok := c.Get(key); ok && val != nil {
} ui64, _ = val.(uint64)
}
// GetFloat32 returns the value associated with the key as a float32. return
func (c *Context) GetFloat32(key string) (f32 float32) {
return getTyped[float32](c, key)
} }
// GetFloat64 returns the value associated with the key as a float64. // GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) { func (c *Context) GetFloat64(key string) (f64 float64) {
return getTyped[float64](c, key) if val, ok := c.Get(key); ok && val != nil {
f64, _ = val.(float64)
}
return
} }
// GetTime returns the value associated with the key as time. // GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key string) (t time.Time) { func (c *Context) GetTime(key string) (t time.Time) {
return getTyped[time.Time](c, key) if val, ok := c.Get(key); ok && val != nil {
t, _ = val.(time.Time)
}
return
} }
// GetDuration returns the value associated with the key as a duration. // GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key string) (d time.Duration) { func (c *Context) GetDuration(key string) (d time.Duration) {
return getTyped[time.Duration](c, key) if val, ok := c.Get(key); ok && val != nil {
} d, _ = val.(time.Duration)
}
// GetIntSlice returns the value associated with the key as a slice of integers. return
func (c *Context) GetIntSlice(key string) (is []int) {
return getTyped[[]int](c, key)
}
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key string) (i8s []int8) {
return getTyped[[]int8](c, key)
}
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key string) (i16s []int16) {
return getTyped[[]int16](c, key)
}
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key string) (i32s []int32) {
return getTyped[[]int32](c, key)
}
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key string) (i64s []int64) {
return getTyped[[]int64](c, key)
}
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key string) (uis []uint) {
return getTyped[[]uint](c, key)
}
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key string) (ui8s []uint8) {
return getTyped[[]uint8](c, key)
}
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key string) (ui16s []uint16) {
return getTyped[[]uint16](c, key)
}
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key string) (ui32s []uint32) {
return getTyped[[]uint32](c, key)
}
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key string) (ui64s []uint64) {
return getTyped[[]uint64](c, key)
}
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key string) (f32s []float32) {
return getTyped[[]float32](c, key)
}
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key string) (f64s []float64) {
return getTyped[[]float64](c, key)
} }
// GetStringSlice returns the value associated with the key as a slice of strings. // GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) { func (c *Context) GetStringSlice(key string) (ss []string) {
return getTyped[[]string](c, key) if val, ok := c.Get(key); ok && val != nil {
ss, _ = val.([]string)
}
return
} }
// GetStringMap returns the value associated with the key as a map of interfaces. // GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]any) { func (c *Context) GetStringMap(key string) (sm map[string]any) {
return getTyped[map[string]any](c, key) if val, ok := c.Get(key); ok && val != nil {
sm, _ = val.(map[string]any)
}
return
} }
// GetStringMapString returns the value associated with the key as a map of strings. // GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key string) (sms map[string]string) { func (c *Context) GetStringMapString(key string) (sms map[string]string) {
return getTyped[map[string]string](c, key) if val, ok := c.Get(key); ok && val != nil {
sms, _ = val.(map[string]string)
}
return
} }
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) { func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
return getTyped[map[string][]string](c, key) if val, ok := c.Get(key); ok && val != nil {
smss, _ = val.(map[string][]string)
}
return
} }
/************************************/ /************************************/
@ -464,12 +382,9 @@ func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string)
// Param returns the value of the URL param. // Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key) // It is a shortcut for c.Params.ByName(key)
//
// router.GET("/user/:id", func(c *gin.Context) { // router.GET("/user/:id", func(c *gin.Context) {
// // a GET request to /user/john // // a GET request to /user/john
// id := c.Param("id") // id == "john" // id := c.Param("id") // id == "john"
// // a GET request to /user/john/
// id := c.Param("id") // id == "/john/"
// }) // })
func (c *Context) Param(key string) string { func (c *Context) Param(key string) string {
return c.Params.ByName(key) return c.Params.ByName(key)
@ -487,7 +402,6 @@ func (c *Context) AddParam(key, value string) {
// Query returns the keyed url query value if it exists, // Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`. // otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)` // It is shortcut for `c.Request.URL.Query().Get(key)`
//
// GET /path?id=1234&name=Manu&value= // GET /path?id=1234&name=Manu&value=
// c.Query("id") == "1234" // c.Query("id") == "1234"
// c.Query("name") == "Manu" // c.Query("name") == "Manu"
@ -501,7 +415,6 @@ func (c *Context) Query(key string) (value string) {
// DefaultQuery returns the keyed url query value if it exists, // DefaultQuery returns the keyed url query value if it exists,
// otherwise it returns the specified defaultValue string. // otherwise it returns the specified defaultValue string.
// See: Query() and GetQuery() for further information. // See: Query() and GetQuery() for further information.
//
// GET /?name=Manu&lastname= // GET /?name=Manu&lastname=
// c.DefaultQuery("name", "unknown") == "Manu" // c.DefaultQuery("name", "unknown") == "Manu"
// c.DefaultQuery("id", "none") == "none" // c.DefaultQuery("id", "none") == "none"
@ -517,7 +430,6 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
// if it exists `(value, true)` (even when the value is an empty string), // if it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns `("", false)`. // otherwise it returns `("", false)`.
// It is shortcut for `c.Request.URL.Query().Get(key)` // It is shortcut for `c.Request.URL.Query().Get(key)`
//
// GET /?name=Manu&lastname= // GET /?name=Manu&lastname=
// ("Manu", true) == c.GetQuery("name") // ("Manu", true) == c.GetQuery("name")
// ("", false) == c.GetQuery("id") // ("", false) == c.GetQuery("id")
@ -538,7 +450,7 @@ func (c *Context) QueryArray(key string) (values []string) {
func (c *Context) initQueryCache() { func (c *Context) initQueryCache() {
if c.queryCache == nil { if c.queryCache == nil {
if c.Request != nil && c.Request.URL != nil { if c.Request != nil {
c.queryCache = c.Request.URL.Query() c.queryCache = c.Request.URL.Query()
} else { } else {
c.queryCache = url.Values{} c.queryCache = url.Values{}
@ -588,7 +500,6 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
// form or multipart form when it exists `(value, true)` (even when the value is an empty string), // form or multipart form when it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns ("", false). // otherwise it returns ("", false).
// For example, during a PATCH request to update the user's email: // For example, during a PATCH request to update the user's email:
//
// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com" // email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
// email= --> ("", true) := GetPostForm("email") // set email to "" // email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email // --> ("", false) := GetPostForm("email") // do nothing with email
@ -640,7 +551,7 @@ func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
return c.get(c.formCache, key) return c.get(c.formCache, key)
} }
// get is an internal method and returns a map which satisfies conditions. // get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) { func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string) dicts := make(map[string]string)
exist := false exist := false
@ -677,25 +588,13 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
} }
// SaveUploadedFile uploads the form file to specific dst. // SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error { func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open() src, err := file.Open()
if err != nil { if err != nil {
return err return err
} }
defer src.Close() defer src.Close()
var mode os.FileMode = 0o750
if len(perm) > 0 {
mode = perm[0]
}
dir := filepath.Dir(dst)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if err = os.Chmod(dir, mode); err != nil {
return err
}
out, err := os.Create(dst) out, err := os.Create(dst)
if err != nil { if err != nil {
return err return err
@ -708,10 +607,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
// Bind checks the Method and Content-Type to select a binding engine automatically, // Bind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example: // Depending on the "Content-Type" header different bindings are used, for example:
//
// "application/json" --> JSON binding // "application/json" --> JSON binding
// "application/xml" --> XML binding // "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer. // It decodes the json payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
@ -741,15 +638,10 @@ func (c *Context) BindYAML(obj any) error {
} }
// BindTOML is a shortcut for c.MustBindWith(obj, binding.TOML). // BindTOML is a shortcut for c.MustBindWith(obj, binding.TOML).
func (c *Context) BindTOML(obj any) error { func (c *Context) BindTOML(obj interface{}) error {
return c.MustBindWith(obj, binding.TOML) return c.MustBindWith(obj, binding.TOML)
} }
// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain).
func (c *Context) BindPlain(obj any) error {
return c.MustBindWith(obj, binding.Plain)
}
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj any) error { func (c *Context) BindHeader(obj any) error {
return c.MustBindWith(obj, binding.Header) return c.MustBindWith(obj, binding.Header)
@ -759,7 +651,7 @@ func (c *Context) BindHeader(obj any) error {
// It will abort the request with HTTP 400 if any error occurs. // It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj any) error { func (c *Context) BindUri(obj any) error {
if err := c.ShouldBindUri(obj); err != nil { if err := c.ShouldBindUri(obj); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err return err
} }
return nil return nil
@ -770,7 +662,7 @@ func (c *Context) BindUri(obj any) error {
// See the binding package. // See the binding package.
func (c *Context) MustBindWith(obj any, b binding.Binding) error { func (c *Context) MustBindWith(obj any, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil { if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err return err
} }
return nil return nil
@ -778,10 +670,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// ShouldBind checks the Method and Content-Type to select a binding engine automatically, // ShouldBind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example: // Depending on the "Content-Type" header different bindings are used, for example:
//
// "application/json" --> JSON binding // "application/json" --> JSON binding
// "application/xml" --> XML binding // "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer. // It decodes the json payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid. // Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
@ -811,15 +701,10 @@ func (c *Context) ShouldBindYAML(obj any) error {
} }
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML). // ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
func (c *Context) ShouldBindTOML(obj any) error { func (c *Context) ShouldBindTOML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.TOML) return c.ShouldBindWith(obj, binding.TOML)
} }
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
func (c *Context) ShouldBindPlain(obj any) error {
return c.ShouldBindWith(obj, binding.Plain)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj any) error { func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header) return c.ShouldBindWith(obj, binding.Header)
@ -827,7 +712,7 @@ func (c *Context) ShouldBindHeader(obj any) error {
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj any) error { func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string, len(c.Params)) m := make(map[string][]string)
for _, v := range c.Params { for _, v := range c.Params {
m[v.Key] = []string{v.Value} m[v.Key] = []string{v.Value}
} }
@ -853,7 +738,7 @@ func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error
} }
} }
if body == nil { if body == nil {
body, err = io.ReadAll(c.Request.Body) body, err = ioutil.ReadAll(c.Request.Body)
if err != nil { if err != nil {
return err return err
} }
@ -862,34 +747,9 @@ func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error
return bb.BindBody(body, obj) return bb.BindBody(body, obj)
} }
// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON).
func (c *Context) ShouldBindBodyWithJSON(obj any) error {
return c.ShouldBindBodyWith(obj, binding.JSON)
}
// ShouldBindBodyWithXML is a shortcut for c.ShouldBindBodyWith(obj, binding.XML).
func (c *Context) ShouldBindBodyWithXML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.XML)
}
// ShouldBindBodyWithYAML is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML).
func (c *Context) ShouldBindBodyWithYAML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.YAML)
}
// ShouldBindBodyWithTOML is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML).
func (c *Context) ShouldBindBodyWithTOML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.TOML)
}
// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain).
func (c *Context) ShouldBindBodyWithPlain(obj any) error {
return c.ShouldBindBodyWith(obj, binding.Plain)
}
// ClientIP implements one best effort algorithm to return the real client IP. // ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. // It called c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]). // If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy, // If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned. // the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
@ -997,10 +857,7 @@ func (c *Context) GetHeader(key string) string {
// GetRawData returns stream data. // GetRawData returns stream data.
func (c *Context) GetRawData() ([]byte, error) { func (c *Context) GetRawData() ([]byte, error) {
if c.Request.Body == nil { return ioutil.ReadAll(c.Request.Body)
return nil, errors.New("cannot read nil body")
}
return io.ReadAll(c.Request.Body)
} }
// SetSameSite with cookie // SetSameSite with cookie
@ -1051,9 +908,7 @@ func (c *Context) Render(code int, r render.Render) {
} }
if err := r.Render(c.Writer); err != nil { if err := r.Render(c.Writer); err != nil {
// Pushing error to c.Errors panic(err)
_ = c.Error(err)
c.Abort()
} }
} }
@ -1122,7 +977,7 @@ func (c *Context) YAML(code int, obj any) {
} }
// TOML serializes the given struct as TOML into the response body. // TOML serializes the given struct as TOML into the response body.
func (c *Context) TOML(code int, obj any) { func (c *Context) TOML(code int, obj interface{}) {
c.Render(code, render.TOML{Data: obj}) c.Render(code, render.TOML{Data: obj})
} }
@ -1179,17 +1034,11 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
http.FileServer(fs).ServeHTTP(c.Writer, c.Request) http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
} }
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
// FileAttachment writes the specified file into the body stream in an efficient way // FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename // On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) { func (c *Context) FileAttachment(filepath, filename string) {
if isASCII(filename) { if isASCII(filename) {
c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+escapeQuotes(filename)+`"`) c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
} else { } else {
c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename))
} }
@ -1254,7 +1103,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data) data := chooseData(config.XMLData, config.Data)
c.XML(code, data) c.XML(code, data)
case binding.MIMEYAML, binding.MIMEYAML2: case binding.MIMEYAML:
data := chooseData(config.YAMLData, config.Data) data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data) c.YAML(code, data)
@ -1263,7 +1112,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
c.TOML(code, data) c.TOML(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
} }
} }
@ -1282,7 +1131,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune // therefore we can just iterate over the string without casting it into []rune
i := 0 i := 0
for ; i < len(accepted) && i < len(offer); i++ { for ; i < len(accepted); i++ {
if accepted[i] == '*' || offer[i] == '*' { if accepted[i] == '*' || offer[i] == '*' {
return offer return offer
} }
@ -1307,16 +1156,9 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/ /************************************/
// hasRequestContext returns whether c.Request has Context and fallback.
func (c *Context) hasRequestContext() bool {
hasFallback := c.engine != nil && c.engine.ContextWithFallback
hasRequestContext := c.Request != nil && c.Request.Context() != nil
return hasFallback && hasRequestContext
}
// Deadline returns that there is no deadline (ok==false) when c.Request has no Context. // Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
func (c *Context) Deadline() (deadline time.Time, ok bool) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
if !c.hasRequestContext() { if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
return return
} }
return c.Request.Context().Deadline() return c.Request.Context().Deadline()
@ -1324,7 +1166,7 @@ func (c *Context) Deadline() (deadline time.Time, ok bool) {
// Done returns nil (chan which will wait forever) when c.Request has no Context. // Done returns nil (chan which will wait forever) when c.Request has no Context.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
if !c.hasRequestContext() { if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
return nil return nil
} }
return c.Request.Context().Done() return c.Request.Context().Done()
@ -1332,7 +1174,7 @@ func (c *Context) Done() <-chan struct{} {
// Err returns nil when c.Request has no Context. // Err returns nil when c.Request has no Context.
func (c *Context) Err() error { func (c *Context) Err() error {
if !c.hasRequestContext() { if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
return nil return nil
} }
return c.Request.Context().Err() return c.Request.Context().Err()
@ -1342,7 +1184,7 @@ func (c *Context) Err() error {
// if no value is associated with key. Successive calls to Value with // if no value is associated with key. Successive calls to Value with
// the same key returns the same result. // the same key returns the same result.
func (c *Context) Value(key any) any { func (c *Context) Value(key any) any {
if key == ContextRequestKey { if key == 0 {
return c.Request return c.Request
} }
if key == ContextKey { if key == ContextKey {
@ -1353,7 +1195,7 @@ func (c *Context) Value(key any) any {
return val return val
} }
} }
if !c.hasRequestContext() { if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
return nil return nil
} }
return c.Request.Context().Value(key) return c.Request.Context().Value(key)

31
context_1.16_test.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2021 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.17
// +build !go1.17
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed16(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}

72
context_1.17_test.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright 2021 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go1.17
// +build go1.17
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
type interceptedWriter struct {
ResponseWriter
b *bytes.Buffer
}
func (i interceptedWriter) WriteHeader(code int) {
i.Header().Del("X-Test")
i.ResponseWriter.WriteHeader(code)
}
func TestContextFormFileFailed17(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
assert.Panics(t, func() {
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
})
}
func TestInterceptedHeader(t *testing.T) {
w := httptest.NewRecorder()
c, r := CreateTestContext(w)
r.Use(func(c *Context) {
i := interceptedWriter{
ResponseWriter: c.Writer,
b: bytes.NewBuffer(nil),
}
c.Writer = i
c.Next()
c.Header("X-Test", "overridden")
c.Writer = i.ResponseWriter
})
r.GET("/", func(c *Context) {
c.Header("X-Test", "original")
c.Header("X-Test-2", "present")
c.String(http.StatusOK, "hello world")
})
c.Request = httptest.NewRequest("GET", "/", nil)
r.HandleContext(c)
// Result() has headers frozen when WriteHeaderNow() has been called
// Compared to this time, this is when the response headers will be flushed
// As response is flushed on c.String, the Header cannot be set by the first
// middleware. Assert this
assert.Equal(t, "", w.Result().Header.Get("X-Test"))
assert.Equal(t, "present", w.Result().Header.Get("X-Test-2"))
}

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 appengine //go:build appengine
// +build appengine
package gin package gin

File diff suppressed because it is too large Load Diff

View File

@ -10,23 +10,19 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
) )
const ginSupportMinGoVer = 21 const ginSupportMinGoVer = 14
// 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 ...interface{})
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
@ -52,19 +48,12 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
} }
func debugPrint(format string, values ...any) { func debugPrint(format string, values ...any) {
if !IsDebugging() { if IsDebugging() {
return
}
if DebugPrintFunc != nil {
DebugPrintFunc(format, values...)
return
}
if !strings.HasSuffix(format, "\n") { if !strings.HasSuffix(format, "\n") {
format += "\n" format += "\n"
} }
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...) 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.14+.
`) `)
} }

View File

@ -5,25 +5,23 @@
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.14+.\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

@ -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.
//
// Deprecated: Use MustBindWith or ShouldBindWith.
func (c *Context) BindWith(obj any, b binding.Binding) error { func (c *Context) BindWith(obj any, b binding.Binding) error {
log.Println(`BindWith(\"any, binding.Binding\") error is going to log.Println(`BindWith(\"interface{}, 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

@ -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

@ -39,7 +39,7 @@ type Error struct {
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 {
@ -124,7 +124,6 @@ func (a errorMsgs) Last() *Error {
// Errors returns an array with all the error messages. // Errors returns an array with 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"))

View File

@ -11,7 +11,6 @@ import (
"github.com/gin-gonic/gin/internal/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) {
@ -36,7 +35,7 @@ func TestError(t *testing.T) {
jsonBytes, _ := json.Marshal(err) jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ //nolint: errcheck err.SetMeta(H{ // nolint: errcheck
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
@ -46,7 +45,7 @@ func TestError(t *testing.T) {
"data": "some data", "data": "some data",
}, err.JSON()) }, err.JSON())
err.SetMeta(H{ //nolint: errcheck err.SetMeta(H{ // nolint: errcheck
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
@ -61,7 +60,7 @@ func TestError(t *testing.T) {
status string status string
data string data string
} }
err.SetMeta(customError{status: "200", data: "other data"}) //nolint: errcheck err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON()) assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
} }
@ -123,7 +122,7 @@ func TestErrorUnwrap(t *testing.T) {
}) })
// check that 'errors.Is()' and 'errors.As()' behave as expected : // check that 'errors.Is()' and 'errors.As()' behave as expected :
require.ErrorIs(t, err, innerErr) assert.True(t, errors.Is(err, innerErr))
var testErr TestErr var testErr TestErr
require.ErrorAs(t, err, &testErr) assert.True(t, errors.As(err, &testErr))
} }

52
fs.go
View File

@ -9,43 +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)
}

135
gin.go
View File

@ -11,22 +11,16 @@ 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"
"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"
"golang.org/x/net/http2/h2c" "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")
@ -46,15 +40,9 @@ var defaultTrustedCIDRs = []*net.IPNet{
}, },
} }
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
// HandlerFunc defines the handler used by gin middleware as return value. // 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
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice. // HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
@ -85,8 +73,6 @@ const (
// PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining // PlatformCloudflare when using Cloudflare's CDN. Trust CF-Connecting-IP for determining
// the client's IP // the client's IP
PlatformCloudflare = "CF-Connecting-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.
@ -180,7 +166,7 @@ type Engine struct {
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:
@ -190,7 +176,7 @@ var _ IRouter = (*Engine)(nil)
// - 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{
@ -217,17 +203,17 @@ func New(opts ...OptionFunc) *Engine {
} }
engine.RouterGroup.engine = engine engine.RouterGroup.engine = engine
engine.pool.New = func() any { engine.pool.New = func() any {
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) Handler() http.Handler {
@ -239,8 +225,8 @@ func (engine *Engine) Handler() http.Handler {
return h2c.NewHandler(engine, h2s) return h2c.NewHandler(engine, h2s)
} }
func (engine *Engine) allocateContext(maxParams uint16) *Context { func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, maxParams) v := make(Params, 0, engine.maxParams)
skippedNodes := make([]skippedNode, 0, engine.maxSections) skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes} return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
} }
@ -321,15 +307,6 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine return engine
} }
// With returns a Engine with the configuration set in the OptionFunc.
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
}
return engine
}
func (engine *Engine) rebuild404Handlers() { func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute) engine.allNoRoute = engine.combineHandlers(engine.noRoute)
} }
@ -353,6 +330,7 @@ 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
} }
@ -388,6 +366,23 @@ 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) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
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
@ -477,26 +472,6 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false 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 {
@ -511,23 +486,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.
@ -537,7 +495,7 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + 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.") "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
} }
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler()) err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
@ -553,7 +511,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + 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.") "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
} }
listener, err := net.Listen("unix", file) listener, err := net.Listen("unix", file)
@ -576,7 +534,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + 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.") "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-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))
@ -589,22 +547,6 @@ 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) {
@ -613,7 +555,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + 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.") "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
} }
err = http.Serve(listener, engine.Handler()) err = http.Serve(listener, engine.Handler())
@ -637,12 +579,10 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Disclaimer: You can loop yourself to deal with this, use wisely. // Disclaimer: You can loop yourself to deal with this, use wisely.
func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index oldIndexValue := c.index
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) {
@ -689,26 +629,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, c.skippedNodes, unescape); value.handlers != nil {
allowed = append(allowed, tree.method)
}
}
if len(allowed) > 0 {
c.handlers = engine.allNoMethod c.handlers = engine.allNoMethod
c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
serveError(c, http.StatusMethodNotAllowed, default405Body) serveError(c, http.StatusMethodNotAllowed, default405Body)
return return
} }
} }
}
c.handlers = engine.allNoRoute c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body) serveError(c, http.StatusNotFound, default404Body)
} }
@ -736,9 +668,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

@ -108,7 +108,6 @@ 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)

View File

@ -9,7 +9,7 @@ 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"
@ -21,7 +21,6 @@ import (
"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) // params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
@ -41,11 +40,11 @@ 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(params[0])
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)
var responseStatus = "200 OK" var responseStatus = "200 OK"
if len(params) > 1 && params[1] != "" { if len(params) > 1 && params[1] != "" {
@ -74,13 +73,13 @@ 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 TestBadTrustedCIDRs(t *testing.T) {
router := New() router := New()
require.Error(t, router.SetTrustedProxies([]string{"hello/world"})) assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
} }
/* legacy tests /* legacy tests
@ -88,7 +87,7 @@ 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) { func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
@ -101,7 +100,7 @@ func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
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") })
require.Error(t, router.RunUnix(unixTestSocket)) assert.Error(t, router.RunUnix(unixTestSocket))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
@ -113,15 +112,15 @@ func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
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()
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") })
require.Error(t, router.RunFd(int(socketFile.Fd()))) assert.Error(t, router.RunFd(int(socketFile.Fd())))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
@ -133,12 +132,12 @@ func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
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") })
require.Error(t, router.RunListener(listener)) assert.Error(t, router.RunListener(listener))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
@ -149,7 +148,7 @@ func TestBadTrustedCIDRsForRunTLS(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.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
} }
*/ */
@ -165,7 +164,7 @@ 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")
} }
@ -202,7 +201,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")
} }
@ -217,14 +216,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"))
}) })
} }
@ -238,7 +237,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")
} }
@ -258,7 +257,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)
@ -272,38 +271,22 @@ 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() { if isWindows() {
// not supported by windows, it is unimplemented now // not supported by windows, it is unimplemented now
require.Error(t, err) assert.Error(t, err)
} else { } else {
require.NoError(t, err) assert.NoError(t, err)
} }
if socketFile == nil { if socketFile == nil {
@ -319,7 +302,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)
@ -333,15 +316,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))
@ -351,7 +334,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)
@ -366,11 +349,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) {
@ -396,14 +379,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()
}() }()
} }
@ -425,6 +401,17 @@ func TestConcurrentHandleContext(t *testing.T) {
// testRequest(t, "http://localhost:8033/example") // testRequest(t, "http://localhost:8033/example")
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.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")
}
func TestTreeRunDynamicRouting(t *testing.T) { func TestTreeRunDynamicRouting(t *testing.T) {
router := New() router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") }) router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
@ -574,28 +561,3 @@ func TestTreeRunDynamicRouting(t *testing.T) {
func isWindows() bool { func isWindows() bool {
return runtime.GOOS == "windows" 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

@ -8,19 +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" "golang.org/x/net/http2"
) )
@ -73,19 +71,19 @@ 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)) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
} }
func TestH2c(t *testing.T) { func TestH2c(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Error(err) fmt.Println(err)
} }
r := Default() r := Default()
r.UseH2C = true r.UseH2C = true
@ -95,14 +93,14 @@ func TestH2c(t *testing.T) {
go func() { go func() {
err := http.Serve(ln, r.Handler()) err := http.Serve(ln, r.Handler())
if err != nil { if err != nil {
t.Log(err) fmt.Println(err)
} }
}() }()
defer ln.Close() defer ln.Close()
url := "http://" + ln.Addr().String() + "/" url := "http://" + ln.Addr().String() + "/"
httpClient := http.Client{ http := http.Client{
Transport: &http2.Transport{ Transport: &http2.Transport{
AllowHTTP: true, AllowHTTP: true,
DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) { DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) {
@ -111,12 +109,12 @@ func TestH2c(t *testing.T) {
}, },
} }
res, err := httpClient.Get(url) res, err := http.Get(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))
} }
@ -131,12 +129,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 +149,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 +176,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,12 +196,12 @@ 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", string(resp))
} }
@ -229,12 +227,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 +247,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 +267,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 +294,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,42 +314,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", 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) {}})
}) })
} }
@ -493,27 +491,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$",
}) })
@ -531,7 +529,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)
}) })
} }
@ -548,10 +546,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 {
@ -564,7 +562,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())
}) })
@ -573,44 +571,6 @@ 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()
@ -619,7 +579,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
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"}) err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -627,7 +587,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"192.168.1.33/33"}) err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
require.Error(t, err) assert.Error(t, err)
} }
// valid ipv4 address // valid ipv4 address
@ -636,7 +596,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies([]string{"192.168.1.33"}) err := r.SetTrustedProxies([]string{"192.168.1.33"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -644,7 +604,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"192.168.1.256"}) err := r.SetTrustedProxies([]string{"192.168.1.256"})
require.Error(t, err) assert.Error(t, err)
} }
// valid ipv6 address // valid ipv6 address
@ -652,7 +612,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
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"}) err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -660,7 +620,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}) err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
require.Error(t, err) assert.Error(t, err)
} }
// valid ipv6 cidr // valid ipv6 cidr
@ -668,7 +628,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"}) err := r.SetTrustedProxies([]string{"::/0"})
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -676,7 +636,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}) err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
require.Error(t, err) assert.Error(t, err)
} }
// valid combination // valid combination
@ -692,7 +652,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.1", "172.16.0.1",
}) })
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -704,7 +664,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.256", "172.16.0.256",
}) })
require.Error(t, err) assert.Error(t, err)
} }
// nil value // nil value
@ -712,7 +672,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies(nil) err := r.SetTrustedProxies(nil)
assert.Nil(t, r.trustedCIDRs) assert.Nil(t, r.trustedCIDRs)
require.NoError(t, err) assert.Nil(t, err)
} }
} }
@ -736,71 +696,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) {
var fc = func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
e.Use(func(c *Context) {
c.Next()
})
}
r := New(fc)
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: 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.Replace(param, "-", "/", -1))
return nil
}
func TestCustomUnmarshalStruct(t *testing.T) {
route := Default()
var request struct {
Birthday Birthday `form:"birthday"`
}
route.GET("/test", func(ctx *Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
req := httptest.NewRequest(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

@ -5,17 +5,15 @@
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,9 +295,9 @@ 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.NotEqual(t, "", person.Name) assert.True(t, person.Name != "")
assert.NotEqual(t, "", person.ID) assert.True(t, person.ID != "")
c.String(http.StatusOK, "ShouldBindUri test OK") c.String(http.StatusOK, "ShouldBindUri test OK")
}) })
@ -319,9 +317,9 @@ 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.NotEqual(t, "", person.Name) assert.True(t, person.Name != "")
assert.NotEqual(t, "", person.ID) assert.True(t, person.ID != "")
c.String(http.StatusOK, "BindUri test OK") c.String(http.StatusOK, "BindUri test OK")
}) })
@ -340,7 +338,7 @@ 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")
@ -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,

49
go.mod
View File

@ -1,46 +1,31 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.23.0 go 1.18
require ( require (
github.com/bytedance/sonic v1.13.1
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.22.1 github.com/go-playground/validator/v10 v10.10.0
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.9.7
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.14
github.com/pelletier/go-toml/v2 v2.2.2 github.com/pelletier/go-toml/v2 v2.0.1
github.com/quic-go/quic-go v0.48.2 github.com/stretchr/testify v1.7.1
github.com/stretchr/testify v1.9.0 github.com/ugorji/go/codec v1.2.7
github.com/ugorji/go/codec v1.2.12 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
golang.org/x/net v0.37.0 google.golang.org/protobuf v1.28.0
google.golang.org/protobuf v1.34.1 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( 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/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/leodido/go-urn v1.2.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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
go.uber.org/mock v0.4.0 // indirect golang.org/x/text v0.3.6 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
) )

156
go.sum
View File

@ -1,114 +1,84 @@
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/bytedance/sonic v1.13.1/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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sync v0.12.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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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

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

View File

@ -2,7 +2,8 @@
// 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 !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64) //go:build !jsoniter && !go_json
// +build !jsoniter,!go_json
package json package json

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 jsoniter //go:build jsoniter
// +build jsoniter
package json package json

View File

@ -1,23 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build sonic && avx && (linux || windows || darwin) && amd64
package json
import "github.com/bytedance/sonic"
var (
json = sonic.ConfigStd
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -47,15 +47,8 @@ type LoggerConfig struct {
// SkipPaths is an url path array which logs are not written. // SkipPaths is an url path array which logs are not written.
// Optional. // Optional.
SkipPaths []string SkipPaths []string
// Skip is a Skipper that indicates which logs should not be written.
// Optional.
Skip Skipper
} }
// Skipper is a function to skip logs based on provided Context
type Skipper func(c *Context) bool
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter // LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
type LogFormatter func(params LogFormatterParams) string type LogFormatter func(params LogFormatterParams) string
@ -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:
@ -248,11 +239,8 @@ 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{ param := LogFormatterParams{
Request: c.Request, Request: c.Request,
isTerm: isTerm, isTerm: isTerm,
@ -278,4 +266,5 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
fmt.Fprint(out, formatter(param)) fmt.Fprint(out, formatter(param))
} }
}
} }

View File

@ -5,10 +5,10 @@
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,21 +41,21 @@ 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()
@ -77,14 +77,14 @@ func TestLogger(t *testing.T) {
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,21 +105,21 @@ 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()
@ -141,14 +141,14 @@ func TestLoggerWithConfig(t *testing.T) {
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,12 +169,12 @@ 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")
} }
@ -182,7 +182,7 @@ func TestLoggerWithFormatter(t *testing.T) {
func TestLoggerWithConfigFormatting(t *testing.T) { func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams var gotParam LogFormatterParams
var gotKeys map[string]any var gotKeys map[string]any
buffer := new(strings.Builder) buffer := new(bytes.Buffer)
router := New() router := New()
router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs() router.engine.trustedCIDRs, _ = router.engine.prepareTrustedCIDRs()
@ -210,12 +210,12 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
gotKeys = c.Keys gotKeys = c.Keys
time.Sleep(time.Millisecond) 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,7 +225,7 @@ 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)
@ -239,7 +239,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 +250,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 +260,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 +271,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,
@ -292,10 +292,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 +310,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 +328,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 +342,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 +358,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.Equal(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.Equal(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 +406,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

@ -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)
@ -149,7 +149,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
}) })
// 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 +175,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)
@ -196,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
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)
@ -211,7 +211,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New() router := New()
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "A" signature += "A"
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) //nolint: errcheck context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
}) })
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "B" signature += "B"
@ -219,7 +219,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,7 +246,7 @@ 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.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))

21
mode.go
View File

@ -8,7 +8,6 @@ import (
"flag" "flag"
"io" "io"
"os" "os"
"sync/atomic"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
) )
@ -36,7 +35,6 @@ 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
@ -44,8 +42,10 @@ 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 ginMode int32 = debugCode var (
var modeName atomic.Value ginMode = debugCode
modeName = DebugMode
)
func init() { func init() {
mode := os.Getenv(EnvGinMode) mode := os.Getenv(EnvGinMode)
@ -63,16 +63,17 @@ func SetMode(value string) {
} }
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,5 +95,5 @@ func EnableJsonDecoderDisallowUnknownFields() {
// Mode returns current gin mode. // Mode returns current gin mode.
func Mode() string { func Mode() string {
return modeName.Load().(string) return modeName
} }

View File

@ -5,8 +5,8 @@
package gin package gin
import ( import (
"flag"
"os" "os"
"sync/atomic"
"testing" "testing"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -18,24 +18,31 @@ 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, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
tmp := flag.CommandLine
flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
SetMode("")
assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode())
flag.CommandLine = tmp
SetMode(DebugMode) 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") })

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

@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -62,9 +63,7 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
if ne, ok := err.(*net.OpError); ok { if ne, ok := err.(*net.OpError); ok {
var se *os.SyscallError var se *os.SyscallError
if errors.As(ne, &se) { if errors.As(ne, &se) {
seStr := strings.ToLower(se.Error()) if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
if strings.Contains(seStr, "broken pipe") ||
strings.Contains(seStr, "connection reset by peer") {
brokenPipe = true brokenPipe = true
} }
} }
@ -92,7 +91,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)
@ -103,7 +102,7 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
} }
func defaultHandleRecovery(c *Context, _ any) { func defaultHandleRecovery(c *Context, err any) {
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)
} }
@ -122,7 +121,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
} }
@ -164,7 +163,7 @@ func function(pc uintptr) []byte {
if period := bytes.Index(name, dot); period >= 0 { if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:] name = name[period+1:]
} }
name = bytes.ReplaceAll(name, centerDot, dot) name = bytes.Replace(name, centerDot, dot, -1)
return name return name
} }

View File

@ -5,6 +5,8 @@
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,7 +85,7 @@ 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)
} }
@ -120,7 +122,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
for errno, expectMsg := range expectMsgs { for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) { t.Run(expectMsg, func(t *testing.T) {
var buf strings.Builder var buf bytes.Buffer
router := New() router := New()
router.Use(RecoveryWithWriter(&buf)) router.Use(RecoveryWithWriter(&buf))
@ -134,7 +136,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)
@ -143,8 +145,8 @@ 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 any) {
errBuffer.WriteString(err.(string)) errBuffer.WriteString(err.(string))
@ -155,7 +157,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")
@ -166,7 +168,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")
@ -177,8 +179,8 @@ 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 any) {
@ -190,7 +192,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")
@ -201,7 +203,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")
@ -212,8 +214,8 @@ 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 any) {
@ -225,7 +227,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")
@ -236,7 +238,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")

10
render/any.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2021 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.18
// +build !go1.18
package render
type any = interface{}

View File

@ -53,8 +53,11 @@ var (
) )
// 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.

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 render package render

View File

@ -15,22 +15,22 @@ 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) _ Render = TOML{}
) )
func writeContentType(w http.ResponseWriter, value []string) { func writeContentType(w http.ResponseWriter, value []string) {

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 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"
) )
@ -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(), buf.String())
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

@ -8,17 +8,14 @@ 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/internal/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample" 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" "google.golang.org/protobuf/proto"
) )
@ -37,17 +34,17 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(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) {
@ -59,7 +56,7 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(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,7 +67,7 @@ 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) {
@ -84,7 +81,7 @@ 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.Equal(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"))
@ -96,7 +93,7 @@ func TestRenderSecureJSON(t *testing.T) {
}} }}
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,7 +104,7 @@ 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) {
@ -121,7 +118,7 @@ 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"))
@ -133,56 +130,11 @@ func TestRenderJsonpJSON(t *testing.T) {
}} }}
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.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]any{
@ -192,7 +144,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
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.Equal(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,7 +156,7 @@ 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) {
@ -216,15 +168,15 @@ func TestRenderAsciiJSON(t *testing.T) {
err := (AsciiJSON{data1}).Render(w1) err := (AsciiJSON{data1}).Render(w1)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(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,7 +185,7 @@ 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) {
@ -243,7 +195,7 @@ func TestRenderPureJSON(t *testing.T) {
"html": "<b>", "html": "<b>",
} }
err := (PureJSON{data}).Render(w) err := (PureJSON{data}).Render(w)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(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"))
} }
@ -281,12 +233,12 @@ 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, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String()) assert.Equal(t, "\"\\na : Easy!\\nb:\\n\\tc: 2\\n\\td: [3, 4]\\n\\t\"\n", w.Body.String())
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"))
} }
type fail struct{} type fail struct{}
@ -299,28 +251,7 @@ func (ft *fail) MarshalYAML() (any, error) {
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
@ -335,12 +266,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"))
} }
@ -349,7 +280,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) {
@ -363,14 +294,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,
@ -380,7 +311,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,
@ -391,7 +322,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{
@ -402,7 +333,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)
@ -417,7 +348,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"))
} }
@ -436,7 +367,7 @@ func TestRenderString(t *testing.T) {
Data: []any{"manu", 2}, Data: []any{"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"))
} }
@ -449,7 +380,7 @@ func TestRenderStringLenZero(t *testing.T) {
Data: []any{}, Data: []any{},
}).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"))
} }
@ -465,7 +396,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
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"))
} }
@ -481,7 +412,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
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"))
} }
@ -500,7 +431,7 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
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"))
} }
@ -519,7 +450,7 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
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"))
} }
@ -549,7 +480,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"))
@ -572,23 +503,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 := []interface{}{"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

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

View File

@ -49,11 +49,7 @@ type responseWriter struct {
status int status int
} }
var _ ResponseWriter = (*responseWriter)(nil) var _ ResponseWriter = &responseWriter{}
func (w *responseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
func (w *responseWriter) reset(writer http.ResponseWriter) { func (w *responseWriter) reset(writer http.ResponseWriter) {
w.ResponseWriter = writer w.ResponseWriter = writer
@ -65,7 +61,6 @@ func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code { if code > 0 && w.status != code {
if w.Written() { if w.Written() {
debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code) debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
return
} }
w.status = code w.status = code
} }

View File

@ -10,7 +10,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// TODO // TODO
@ -31,12 +30,6 @@ func init() {
SetMode(TestMode) SetMode(TestMode)
} }
func TestResponseWriterUnwrap(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{ResponseWriter: testWriter}
assert.Same(t, testWriter, writer.Unwrap())
}
func TestResponseWriterReset(t *testing.T) { func TestResponseWriterReset(t *testing.T) {
testWriter := httptest.NewRecorder() testWriter := httptest.NewRecorder()
writer := &responseWriter{} writer := &responseWriter{}
@ -96,13 +89,13 @@ func TestResponseWriterWrite(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWriter.Code) assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWriter.Body.String()) assert.Equal(t, "hola", testWriter.Body.String())
require.NoError(t, err) assert.NoError(t, err)
n, err = w.Write([]byte(" adios")) n, err = w.Write([]byte(" adios"))
assert.Equal(t, 6, n) assert.Equal(t, 6, n)
assert.Equal(t, 10, w.Size()) assert.Equal(t, 10, w.Size())
assert.Equal(t, "hola adios", testWriter.Body.String()) assert.Equal(t, "hola adios", testWriter.Body.String())
require.NoError(t, err) assert.NoError(t, err)
} }
func TestResponseWriterHijack(t *testing.T) { func TestResponseWriterHijack(t *testing.T) {
@ -113,7 +106,7 @@ func TestResponseWriterHijack(t *testing.T) {
assert.Panics(t, func() { assert.Panics(t, func() {
_, _, err := w.Hijack() _, _, err := w.Hijack()
require.NoError(t, err) assert.NoError(t, err)
}) })
assert.True(t, w.Written()) assert.True(t, w.Written())
@ -136,54 +129,6 @@ func TestResponseWriterFlush(t *testing.T) {
// should return 500 // should return 500
resp, err := http.Get(testServer.URL) resp, err := http.Get(testServer.URL)
require.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
} }
func TestResponseWriterStatusCode(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{}
writer.reset(testWriter)
w := ResponseWriter(writer)
w.WriteHeader(http.StatusOK)
w.WriteHeaderNow()
assert.Equal(t, http.StatusOK, w.Status())
assert.True(t, w.Written())
w.WriteHeader(http.StatusUnauthorized)
// status must be 200 although we tried to change it
assert.Equal(t, http.StatusOK, w.Status())
}
// mockPusherResponseWriter is an http.ResponseWriter that implements http.Pusher.
type mockPusherResponseWriter struct {
http.ResponseWriter
}
func (m *mockPusherResponseWriter) Push(target string, opts *http.PushOptions) error {
return nil
}
// nonPusherResponseWriter is an http.ResponseWriter that does not implement http.Pusher.
type nonPusherResponseWriter struct {
http.ResponseWriter
}
func TestPusherWithPusher(t *testing.T) {
rw := &mockPusherResponseWriter{}
w := &responseWriter{ResponseWriter: rw}
pusher := w.Pusher()
assert.NotNil(t, pusher, "Expected pusher to be non-nil")
}
func TestPusherWithoutPusher(t *testing.T) {
rw := &nonPusherResponseWriter{}
w := &responseWriter{ResponseWriter: rw}
pusher := w.Pusher()
assert.Nil(t, pusher, "Expected pusher to be nil")
}

View File

@ -42,7 +42,6 @@ type IRoutes interface {
PUT(string, ...HandlerFunc) IRoutes PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes HEAD(string, ...HandlerFunc) IRoutes
Match([]string, string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes StaticFile(string, string) IRoutes
StaticFileFS(string, string, http.FileSystem) IRoutes StaticFileFS(string, string, http.FileSystem) IRoutes
@ -59,7 +58,7 @@ type RouterGroup struct {
root bool root bool
} }
var _ IRouter = (*RouterGroup)(nil) var _ IRouter = &RouterGroup{}
// Use adds middleware to the group, see example code in GitHub. // Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
@ -107,37 +106,37 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha
return group.handle(httpMethod, relativePath, handlers) return group.handle(httpMethod, relativePath, handlers)
} }
// POST is a shortcut for router.Handle("POST", path, handlers). // POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers) return group.handle(http.MethodPost, relativePath, handlers)
} }
// GET is a shortcut for router.Handle("GET", path, handlers). // GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers) return group.handle(http.MethodGet, relativePath, handlers)
} }
// DELETE is a shortcut for router.Handle("DELETE", path, handlers). // DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodDelete, relativePath, handlers) return group.handle(http.MethodDelete, relativePath, handlers)
} }
// PATCH is a shortcut for router.Handle("PATCH", path, handlers). // PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPatch, relativePath, handlers) return group.handle(http.MethodPatch, relativePath, handlers)
} }
// PUT is a shortcut for router.Handle("PUT", path, handlers). // PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPut, relativePath, handlers) return group.handle(http.MethodPut, relativePath, handlers)
} }
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handlers). // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodOptions, relativePath, handlers) return group.handle(http.MethodOptions, relativePath, handlers)
} }
// HEAD is a shortcut for router.Handle("HEAD", path, handlers). // HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodHead, relativePath, handlers) return group.handle(http.MethodHead, relativePath, handlers)
} }
@ -152,15 +151,6 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRou
return group.returnObj() return group.returnObj()
} }
// Match registers a route that matches the specified methods that you declared.
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
for _, method := range methods {
group.handle(method, relativePath, handlers)
}
return group.returnObj()
}
// StaticFile registers a single route in order to serve a single file of the local filesystem. // StaticFile registers a single route in order to serve a single file of the local filesystem.
// router.StaticFile("favicon.ico", "./resources/favicon.ico") // router.StaticFile("favicon.ico", "./resources/favicon.ico")
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
@ -171,7 +161,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.. // StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false}) // router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
// Gin by default uses: gin.Dir() // Gin by default user: gin.Dir()
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes { func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
return group.staticFileHandler(relativePath, func(c *Context) { return group.staticFileHandler(relativePath, func(c *Context) {
c.FileFromFS(filepath, fs) c.FileFromFS(filepath, fs)
@ -192,14 +182,13 @@ func (group *RouterGroup) staticFileHandler(relativePath string, handler Handler
// 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 (group *RouterGroup) Static(relativePath, root string) IRoutes { func (group *RouterGroup) Static(relativePath, root string) IRoutes {
return group.StaticFS(relativePath, Dir(root, false)) return group.StaticFS(relativePath, Dir(root, false))
} }
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead. // StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
// Gin by default uses: gin.Dir() // Gin by default user: gin.Dir()
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static folder") panic("URL parameters can not be used when serving a static folder")
@ -218,7 +207,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) { return func(c *Context) {
if _, noListing := fs.(*OnlyFilesFS); noListing { if _, noListing := fs.(*onlyFilesFS); noListing {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
} }

View File

@ -186,7 +186,6 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
assert.Equal(t, r, r.PUT("/", handler)) assert.Equal(t, r, r.PUT("/", handler))
assert.Equal(t, r, r.OPTIONS("/", handler)) assert.Equal(t, r, r.OPTIONS("/", handler))
assert.Equal(t, r, r.HEAD("/", handler)) assert.Equal(t, r, r.HEAD("/", handler))
assert.Equal(t, r, r.Match([]string{http.MethodPut, http.MethodPatch}, "/match", handler))
assert.Equal(t, r, r.StaticFile("/file", ".")) assert.Equal(t, r, r.StaticFile("/file", "."))
assert.Equal(t, r, r.StaticFileFS("/static2", ".", Dir(".", false))) assert.Equal(t, r, r.StaticFileFS("/static2", ".", Dir(".", false)))

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -13,7 +14,6 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type header struct { type header struct {
@ -181,58 +181,10 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"}) w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location")) assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, 301, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"}) w = PerformRequest(router, http.MethodGet, "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, 200, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api#?"})
assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"})
assert.Equal(t, "//path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"})
assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: "/../../gin-gonic.com"})
assert.Equal(t, "/gin-goniccom/path2/", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"})
assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"})
assert.Equal(t, "api/api/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"})
assert.Equal(t, "/nor-mal/a1/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/%2e%2e/"})
assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location"))
assert.Equal(t, http.StatusMovedPermanently, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
@ -338,56 +290,17 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
assert.Equal(t, "/is/super/great", wild) assert.Equal(t, "/is/super/great", wild)
} }
// TestRouteParamsNotEmpty tests that context parameters will be set
// even if a route with params/wildcards is registered after the context
// initialisation (which happened in a previous requests).
func TestRouteParamsNotEmpty(t *testing.T) {
name := ""
lastName := ""
wild := ""
router := New()
w := PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusNotFound, w.Code)
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
name = c.Params.ByName("name")
lastName = c.Params.ByName("last_name")
var ok bool
wild, ok = c.Params.Get("wild")
assert.True(t, ok)
assert.Equal(t, name, c.Param("name"))
assert.Equal(t, lastName, c.Param("last_name"))
assert.Empty(t, c.Param("wtf"))
assert.Empty(t, c.Params.ByName("wtf"))
wtf, ok := c.Params.Get("wtf")
assert.Empty(t, wtf)
assert.False(t, ok)
})
w = PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
assert.Equal(t, "smith", lastName)
assert.Equal(t, "/is/super/great", wild)
}
// TestHandleStaticFile - ensure the static file handles properly // TestHandleStaticFile - ensure the static file handles properly
func TestRouteStaticFile(t *testing.T) { func TestRouteStaticFile(t *testing.T) {
// SETUP file // SETUP file
testRoot, _ := os.Getwd() testRoot, _ := os.Getwd()
f, err := os.CreateTemp(testRoot, "") f, err := ioutil.TempFile(testRoot, "")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
require.NoError(t, err) assert.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -416,13 +329,13 @@ func TestRouteStaticFile(t *testing.T) {
func TestRouteStaticFileFS(t *testing.T) { func TestRouteStaticFileFS(t *testing.T) {
// SETUP file // SETUP file
testRoot, _ := os.Getwd() testRoot, _ := os.Getwd()
f, err := os.CreateTemp(testRoot, "") f, err := ioutil.TempFile(testRoot, "")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
require.NoError(t, err) assert.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -485,7 +398,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16, // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8' // else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified")) assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
} }
@ -515,18 +428,6 @@ func TestRouteNotAllowedEnabled2(t *testing.T) {
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
func TestRouteNotAllowedEnabled3(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
router.GET("/path", func(c *Context) {})
router.POST("/path", func(c *Context) {})
w := PerformRequest(router, http.MethodPut, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
allowed := w.Header().Get("Allow")
assert.Contains(t, allowed, http.MethodGet)
assert.Contains(t, allowed, http.MethodPost)
}
func TestRouteNotAllowedDisabled(t *testing.T) { func TestRouteNotAllowedDisabled(t *testing.T) {
router := New() router := New()
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
@ -557,10 +458,10 @@ func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound {"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := PerformRequest(router, http.MethodGet, tr.route) w := PerformRequest(router, "GET", tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, w.Header().Get("Location")) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
} }
} }
} }
@ -590,7 +491,7 @@ func TestRouterNotFound(t *testing.T) {
w := PerformRequest(router, http.MethodGet, tr.route) w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, w.Header().Get("Location")) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
} }
} }
@ -620,11 +521,11 @@ func TestRouterNotFound(t *testing.T) {
router = New() router = New()
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
if c.Request.RequestURI == "/login" { if c.Request.RequestURI == "/login" {
c.String(http.StatusOK, "login") c.String(200, "login")
} }
}) })
router.GET("/logout", func(c *Context) { router.GET("/logout", func(c *Context) {
c.String(http.StatusOK, "logout") c.String(200, "logout")
}) })
w = PerformRequest(router, http.MethodGet, "/login") w = PerformRequest(router, http.MethodGet, "/login")
assert.Equal(t, "login", w.Body.String()) assert.Equal(t, "login", w.Body.String())
@ -636,7 +537,7 @@ func TestRouterStaticFSNotFound(t *testing.T) {
router := New() router := New()
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/"))) router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
c.String(http.StatusNotFound, "non existent") c.String(404, "non existent")
}) })
w := PerformRequest(router, http.MethodGet, "/nonexistent") w := PerformRequest(router, http.MethodGet, "/nonexistent")
@ -719,12 +620,12 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
func TestRouteServeErrorWithWriteHeader(t *testing.T) { func TestRouteServeErrorWithWriteHeader(t *testing.T) {
route := New() route := New()
route.Use(func(c *Context) { route.Use(func(c *Context) {
c.Status(http.StatusMisdirectedRequest) c.Status(421)
c.Next() c.Next()
}) })
w := PerformRequest(route, http.MethodGet, "/NotFound") w := PerformRequest(route, http.MethodGet, "/NotFound")
assert.Equal(t, http.StatusMisdirectedRequest, w.Code) assert.Equal(t, 421, w.Code)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
@ -770,22 +671,3 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/not-found") w := PerformRequest(router, http.MethodGet, "/not-found")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) {
r := New()
r.HandleMethodNotAllowed = true
base := r.Group("base")
base.GET("/metrics", handlerTest1)
v1 := base.Group("v1")
v1.GET("/:id/devices", handlerTest1)
v1.GET("/user/:id/groups", handlerTest1)
v1.GET("/orgs/:id", handlerTest1)
v1.DELETE("/orgs/:id", handlerTest1)
w := PerformRequest(r, http.MethodGet, "/base/v1/user/groups")
assert.Equal(t, http.StatusNotFound, w.Code)
}

View File

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

10
testdata/protoexample/any.go vendored Normal file
View File

@ -0,0 +1,10 @@
// Copyright 2021 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.18
// +build !go1.18
package protoexample
type any = interface{}

101
tree.go
View File

@ -65,10 +65,17 @@ func (trees methodTrees) get(method string) *node {
return nil return nil
} }
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func longestCommonPrefix(a, b string) int { func longestCommonPrefix(a, b string) int {
i := 0 i := 0
max_ := min(len(a), len(b)) max := min(len(a), len(b))
for i < max_ && a[i] == b[i] { for i < max && a[i] == b[i] {
i++ i++
} }
return i return i
@ -100,8 +107,7 @@ func countSections(path string) uint16 {
type nodeType uint8 type nodeType uint8
const ( const (
static nodeType = iota root nodeType = iota + 1
root
param param
catchAll catchAll
) )
@ -167,7 +173,6 @@ walk:
child := node{ child := node{
path: n.path[i:], path: n.path[i:],
wildChild: n.wildChild, wildChild: n.wildChild,
nType: static,
indices: n.indices, indices: n.indices,
children: n.children, children: n.children,
handlers: n.handlers, handlers: n.handlers,
@ -198,7 +203,7 @@ walk:
} }
// Check if a child with the next path byte exists // Check if a child with the next path byte exists
for i, max_ := 0, len(n.indices); i < max_; i++ { for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] { if c == n.indices[i] {
parentFullPathIndex += len(n.path) parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) i = n.incrementChildPrio(i)
@ -262,19 +267,7 @@ walk:
// Returns -1 as index, if no wildcard was found. // Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) { func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start // Find start
escapeColon := false
for start, c := range []byte(path) { for start, c := range []byte(path) {
if escapeColon {
escapeColon = false
if c == ':' {
continue
}
panic("invalid escape string in path '" + path + "'")
}
if c == '\\' {
escapeColon = true
continue
}
// A wildcard starts with ':' (param) or '*' (catch-all) // A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' { if c != ':' && c != '*' {
continue continue
@ -356,10 +349,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
} }
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := "" pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]
if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
}
panic("catch-all wildcard '" + path + panic("catch-all wildcard '" + path +
"' in new path '" + fullPath + "' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg + "' conflicts with existing path segment '" + pathSeg +
@ -369,7 +359,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
// currently fixed width 1 for '/' // currently fixed width 1 for '/'
i-- i--
if i < 0 || path[i] != '/' { if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'") panic("no / before catch-all in path '" + fullPath + "'")
} }
@ -465,11 +455,11 @@ walk: // Outer loop for walking the tree
if !n.wildChild { if !n.wildChild {
// If the path at the end of the loop is not equal to '/' and the current node has no child nodes // If the path at the end of the loop is not equal to '/' and the current node has no child nodes
// the current node needs to roll back to last valid skippedNode // the current node needs to roll back to last vaild skippedNode
if path != "/" { if path != "/" {
for length := len(*skippedNodes); length > 0; length-- { for l := len(*skippedNodes); l > 0; {
skippedNode := (*skippedNodes)[length-1] skippedNode := (*skippedNodes)[l-1]
*skippedNodes = (*skippedNodes)[:length-1] *skippedNodes = (*skippedNodes)[:l-1]
if strings.HasSuffix(skippedNode.path, path) { if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path path = skippedNode.path
n = skippedNode.node n = skippedNode.node
@ -486,7 +476,7 @@ walk: // Outer loop for walking the tree
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
value.tsr = path == "/" && n.handlers != nil value.tsr = path == "/" && n.handlers != nil
return value return
} }
// Handle wildcard child, which is always at the end of the array // Handle wildcard child, which is always at the end of the array
@ -505,14 +495,7 @@ walk: // Outer loop for walking the tree
} }
// Save param value // Save param value
if params != nil { if params != nil && cap(*params) > 0 {
// Preallocate capacity if necessary
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
*params = newParams
}
if value.params == nil { if value.params == nil {
value.params = params value.params = params
} }
@ -541,12 +524,12 @@ walk: // Outer loop for walking the tree
// ... but we can't // ... but we can't
value.tsr = len(path) == end+1 value.tsr = len(path) == end+1
return value return
} }
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath value.fullPath = n.fullPath
return value return
} }
if len(n.children) == 1 { if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
@ -554,18 +537,11 @@ walk: // Outer loop for walking the tree
n = n.children[0] n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
} }
return value return
case catchAll: case catchAll:
// Save param value // Save param value
if params != nil { if params != nil {
// Preallocate capacity if necessary
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
*params = newParams
}
if value.params == nil { if value.params == nil {
value.params = params value.params = params
} }
@ -586,7 +562,7 @@ walk: // Outer loop for walking the tree
value.handlers = n.handlers value.handlers = n.handlers
value.fullPath = n.fullPath value.fullPath = n.fullPath
return value return
default: default:
panic("invalid node type") panic("invalid node type")
@ -596,11 +572,11 @@ walk: // Outer loop for walking the tree
if path == prefix { if path == prefix {
// If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node // If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
// the current node needs to roll back to last valid skippedNode // the current node needs to roll back to last vaild skippedNode
if n.handlers == nil && path != "/" { if n.handlers == nil && path != "/" {
for length := len(*skippedNodes); length > 0; length-- { for l := len(*skippedNodes); l > 0; {
skippedNode := (*skippedNodes)[length-1] skippedNode := (*skippedNodes)[l-1]
*skippedNodes = (*skippedNodes)[:length-1] *skippedNodes = (*skippedNodes)[:l-1]
if strings.HasSuffix(skippedNode.path, path) { if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path path = skippedNode.path
n = skippedNode.node n = skippedNode.node
@ -617,7 +593,7 @@ walk: // Outer loop for walking the tree
// Check if this node has a handle registered. // Check if this node has a handle registered.
if value.handlers = n.handlers; value.handlers != nil { if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath value.fullPath = n.fullPath
return value return
} }
// If there is no handle for this route, but this route has a // If there is no handle for this route, but this route has a
@ -625,12 +601,7 @@ walk: // Outer loop for walking the tree
// additional trailing slash // additional trailing slash
if path == "/" && n.wildChild && n.nType != root { if path == "/" && n.wildChild && n.nType != root {
value.tsr = true value.tsr = true
return value return
}
if path == "/" && n.nType == static {
value.tsr = true
return value
} }
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
@ -640,11 +611,11 @@ walk: // Outer loop for walking the tree
n = n.children[i] n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) || value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil) (n.nType == catchAll && n.children[0].handlers != nil)
return value return
} }
} }
return value return
} }
// Nothing found. We can recommend to redirect to the same URL with an // Nothing found. We can recommend to redirect to the same URL with an
@ -655,9 +626,9 @@ walk: // Outer loop for walking the tree
// roll back to last valid skippedNode // roll back to last valid skippedNode
if !value.tsr && path != "/" { if !value.tsr && path != "/" {
for length := len(*skippedNodes); length > 0; length-- { for l := len(*skippedNodes); l > 0; {
skippedNode := (*skippedNodes)[length-1] skippedNode := (*skippedNodes)[l-1]
*skippedNodes = (*skippedNodes)[:length-1] *skippedNodes = (*skippedNodes)[:l-1]
if strings.HasSuffix(skippedNode.path, path) { if strings.HasSuffix(skippedNode.path, path) {
path = skippedNode.path path = skippedNode.path
n = skippedNode.node n = skippedNode.node
@ -670,7 +641,7 @@ walk: // Outer loop for walking the tree
} }
} }
return value return
} }
} }
@ -775,7 +746,7 @@ walk: // Outer loop for walking the tree
// Runes are up to 4 byte long, // Runes are up to 4 byte long,
// -4 would definitely be another rune. // -4 would definitely be another rune.
var off int var off int
for max_ := min(npLen, 3); off < max_; off++ { for max := min(npLen, 3); off < max; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) { if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path // read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:]) rv, _ = utf8.DecodeRuneInString(oldPath[i:])

View File

@ -192,7 +192,6 @@ func TestTreeWildcard(t *testing.T) {
"/get/abc/123abg/:param", "/get/abc/123abg/:param",
"/get/abc/123abf/:param", "/get/abc/123abf/:param",
"/get/abc/123abfff/:param", "/get/abc/123abfff/:param",
"/get/abc/escaped_colon/test\\:param",
} }
for _, route := range routes { for _, route := range routes {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
@ -316,7 +315,6 @@ func TestTreeWildcard(t *testing.T) {
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}}, {"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}}, {"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}}, {"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
@ -419,11 +417,6 @@ func TestTreeWildcardConflict(t *testing.T) {
{"/user_:name", false}, {"/user_:name", false},
{"/id:id", false}, {"/id:id", false},
{"/id/:id", false}, {"/id/:id", false},
{"/static/*file", false},
{"/static/", true},
{"/escape/test\\:d1", false},
{"/escape/test\\:d2", false},
{"/escape/test:param", false},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -691,26 +684,6 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
} }
} }
func TestRedirectTrailingSlash(t *testing.T) {
var data = []struct {
path string
}{
{"/hello/:name"},
{"/hello/:name/123"},
{"/hello/:name/234"},
}
node := &node{}
for _, item := range data {
node.addRoute(item.path, fakeHandler("test"))
}
value := node.getValue("/hello/abx/", nil, getSkippedNodes(), false)
if value.tsr != true {
t.Fatalf("want true, is false")
}
}
func TestTreeFindCaseInsensitivePath(t *testing.T) { func TestTreeFindCaseInsensitivePath(t *testing.T) {
tree := &node{} tree := &node{}
@ -900,9 +873,9 @@ func TestTreeInvalidNodeType(t *testing.T) {
func TestTreeInvalidParamsType(t *testing.T) { func TestTreeInvalidParamsType(t *testing.T) {
tree := &node{} tree := &node{}
// add a child with wildcard tree.wildChild = true
route := "/:path" tree.children = append(tree.children, &node{})
tree.addRoute(route, fakeHandler(route)) tree.children[0].nType = 2
// set invalid Params type // set invalid Params type
params := make(Params, 0) params := make(Params, 0)
@ -911,34 +884,6 @@ func TestTreeInvalidParamsType(t *testing.T) {
tree.getValue("/test", &params, getSkippedNodes(), false) tree.getValue("/test", &params, getSkippedNodes(), false)
} }
func TestTreeExpandParamsCapacity(t *testing.T) {
data := []struct {
path string
}{
{"/:path"},
{"/*path"},
}
for _, item := range data {
tree := &node{}
tree.addRoute(item.path, fakeHandler(item.path))
params := make(Params, 0)
value := tree.getValue("/test", &params, getSkippedNodes(), false)
if value.params == nil {
t.Errorf("Expected %s params to be set, but they weren't", item.path)
continue
}
if len(*value.params) != 1 {
t.Errorf("Wrong number of %s params: got %d, want %d",
item.path, len(*value.params), 1)
continue
}
}
}
func TestTreeWildcardConflictEx(t *testing.T) { func TestTreeWildcardConflictEx(t *testing.T) {
conflicts := [...]struct { conflicts := [...]struct {
route string route string
@ -976,45 +921,3 @@ func TestTreeWildcardConflictEx(t *testing.T) {
} }
} }
} }
func TestTreeInvalidEscape(t *testing.T) {
routes := map[string]bool{
"/r1/r": true,
"/r2/:r": true,
"/r3/\\:r": true,
}
tree := &node{}
for route, valid := range routes {
recv := catchPanic(func() {
tree.addRoute(route, fakeHandler(route))
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
}
}
func TestWildcardInvalidSlash(t *testing.T) {
const panicMsgPrefix = "no / before catch-all in path"
routes := map[string]bool{
"/foo/bar": true,
"/foo/x*zy": false,
"/foo/b*r": false,
}
for route, valid := range routes {
tree := &node{}
recv := catchPanic(func() {
tree.addRoute(route, nil)
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
if rs, ok := recv.(string); recv != nil && (!ok || !strings.HasPrefix(rs, panicMsgPrefix)) {
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsgPrefix, route, recv)
}
}
}

View File

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

View File

@ -29,7 +29,7 @@ type testStruct struct {
} }
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
assert.Equal(t.T, http.MethodPost, req.Method) assert.Equal(t.T, "POST", req.Method)
assert.Equal(t.T, "/path", req.URL.Path) assert.Equal(t.T, "/path", req.URL.Path)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "hello") fmt.Fprint(w, "hello")
@ -39,17 +39,17 @@ func TestWrap(t *testing.T) {
router := New() router := New()
router.POST("/path", WrapH(&testStruct{t})) router.POST("/path", WrapH(&testStruct{t}))
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) { router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "GET", req.Method)
assert.Equal(t, "/path2", req.URL.Path) assert.Equal(t, "/path2", req.URL.Path)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "hola!") fmt.Fprint(w, "hola!")
})) }))
w := PerformRequest(router, http.MethodPost, "/path") w := PerformRequest(router, "POST", "/path")
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hello", w.Body.String()) assert.Equal(t, "hello", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/path2") w = PerformRequest(router, "GET", "/path2")
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "hola!", w.Body.String()) assert.Equal(t, "hola!", w.Body.String())
} }
@ -119,13 +119,13 @@ func TestBindMiddleware(t *testing.T) {
called = true called = true
value = c.MustGet(BindKey).(*bindTestStruct) value = c.MustGet(BindKey).(*bindTestStruct)
}) })
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=10") PerformRequest(router, "GET", "/?foo=hola&bar=10")
assert.True(t, called) assert.True(t, called)
assert.Equal(t, "hola", value.Foo) assert.Equal(t, "hola", value.Foo)
assert.Equal(t, 10, value.Bar) assert.Equal(t, 10, value.Bar)
called = false called = false
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=1") PerformRequest(router, "GET", "/?foo=hola&bar=1")
assert.False(t, called) assert.False(t, called)
assert.Panics(t, func() { assert.Panics(t, func() {
@ -145,6 +145,6 @@ func TestMarshalXMLforH(t *testing.T) {
} }
func TestIsASCII(t *testing.T) { func TestIsASCII(t *testing.T) {
assert.True(t, isASCII("test")) assert.Equal(t, isASCII("test"), true)
assert.False(t, isASCII("🧡💛💚💙💜")) assert.Equal(t, isASCII("🧡💛💚💙💜"), false)
} }

View File

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