Merge branch 'master' into thread-safe-headers

This commit is contained in:
Bo-Yi Wu 2025-12-05 10:34:33 +08:00 committed by GitHub
commit 3a07160a18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1724 additions and 401 deletions

View File

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

60
.github/ISSUE_TEMPLATE/bug-report.yaml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: ["type/bug"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to appleboy.tw@gmail.com instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions problems on our Discussions Forum (https://github.com/gin-gonic/gin/discussions).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
- type: input
id: gin-ver
attributes:
label: Gin Version
description: Gin version (or commit reference) of your instance
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug?
description: |
If so, please write the steps to reproduce the bug.
options:
- "Yes"
- "No"
validations:
required: true
- type: markdown
attributes:
value: |
It's really important to provide pertinent logs
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
- type: textarea
id: source-code
attributes:
label: Source Code
description: If this issue involves source code, please provide a minimal reproducible example
- type: input
id: go-ver
attributes:
label: Go Version
description: The version of Go running on the server
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to run Gin

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Go.dev API Documentation
url: https://pkg.go.dev/github.com/gin-gonic/gin
about: Comprehensive API documentation for Gin.
- name: Gin User Guides
url: https://gin-gonic.com/
about: In-depth user guides and tutorials for using Gin.
- name: Discussions Forum
url: https://github.com/gin-gonic/gin/discussions
about: Questions and configuration or deployment problems can also be discussed.

View File

@ -0,0 +1,18 @@
name: Feature Request
description: Got an idea for a feature that Gin doesn't have currently? Submit your idea here!
labels: ["type/proposal"]
body:
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions problems on our Discussions Forum (https://github.com/gin-gonic/gin/discussions).
3. Please take a moment to check that your feature hasn't already been suggested.
- type: textarea
id: description
attributes:
label: Feature Description
placeholder: |
I think it would be great if Gin had...
validations:
required: true

View File

@ -1,7 +1,10 @@
- With pull requests: # Pull Request Checklist
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
Please ensure your pull request meets the following requirements:
- [ ] Open your pull request against the `master` branch.
- [ ] All tests pass in available continuous integration systems (e.g., GitHub Actions).
- [ ] Tests are added or modified as needed to cover code changes.
- [ ] If the pull request introduces a new feature, the feature is documented in the `docs/doc.md`.
Thank you for contributing!

View File

@ -1,10 +1,14 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod - package-ecosystem: gomod
directory: / directory: /
schedule: schedule:
interval: weekly interval: daily
- package-ecosystem: github-actions
directory: /
groups:
actions:
patterns:
- "*"
schedule:
interval: daily

View File

@ -33,11 +33,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
# 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@v4
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@v4

View File

@ -16,24 +16,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1" go-version: "^1"
- name: Setup golangci-lint - name: Setup golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.1.6 version: v2.6
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.24", "1.25"]
test-tags: test-tags:
[ [
"", "",
@ -55,13 +55,13 @@ 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@v6
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
cache: false cache: false
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
@ -78,6 +78,6 @@ 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@v5
with: with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }} flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}

View File

@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "^1" go-version: "^1"
- name: Run GoReleaser - name: Run GoReleaser

56
.github/workflows/trivy-scan.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run daily at 00:00 UTC
- cron: '0 0 * * *'
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
security-events: write # Required for uploading SARIF results
jobs:
trivy-scan:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.33.1
if: always()
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'table'
severity: 'CRITICAL,HIGH,MEDIUM'
ignore-unfixed: true
exit-code: '1'

View File

@ -18,15 +18,8 @@ linters:
- wastedassign - wastedassign
settings: settings:
gosec: gosec:
includes: excludes:
- G102 - G115
- G106
- G108
- G109
- G111
- G112
- G201
- G203
perfsprint: perfsprint:
int-conversion: true int-conversion: true
err-error: true err-error: true
@ -68,7 +61,6 @@ linters:
- examples$ - examples$
formatters: formatters:
enable: enable:
- gci
- gofmt - gofmt
- gofumpt - gofumpt
- goimports - goimports
@ -80,7 +72,4 @@ formatters:
exclusions: exclusions:
generated: lax generated: lax
paths: paths:
- third_party$
- builtin$
- examples$
- gin.go - gin.go

View File

@ -1,5 +1,99 @@
# Gin ChangeLog # Gin ChangeLog
## Gin v1.11.0
### Features
* feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210))
* feat(form): add array collection format in form binding ([#3986](https://github.com/gin-gonic/gin/pull/3986)), add custom string slice for form tag unmarshal ([#3970](https://github.com/gin-gonic/gin/pull/3970))
* feat(binding): add BindPlain ([#3904](https://github.com/gin-gonic/gin/pull/3904))
* feat(fs): Export, test and document OnlyFilesFS ([#3939](https://github.com/gin-gonic/gin/pull/3939))
* feat(binding): add support for unixMilli and unixMicro ([#4190](https://github.com/gin-gonic/gin/pull/4190))
* feat(form): Support default values for collections in form binding ([#4048](https://github.com/gin-gonic/gin/pull/4048))
* feat(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633))
### Enhancements
* perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339))
* refactor(tree): replace string(/) with "/" in node.insertChild ([#4354](https://github.com/gin-gonic/gin/pull/4354))
* refactor(render): remove headers parameter from writeHeader ([#4353](https://github.com/gin-gonic/gin/pull/4353))
* refactor(context): simplify "GetType()" functions ([#4080](https://github.com/gin-gonic/gin/pull/4080))
* refactor(slice): simplify SliceValidationError Error method ([#3910](https://github.com/gin-gonic/gin/pull/3910))
* refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile ([#4181](https://github.com/gin-gonic/gin/pull/4181))
* refactor(context): refactor context handling and improve test robustness ([#4066](https://github.com/gin-gonic/gin/pull/4066))
* refactor(binding): use strings.Cut to replace strings.Index ([#3522](https://github.com/gin-gonic/gin/pull/3522))
* refactor(context): add an optional permission parameter to SaveUploadedFile ([#4068](https://github.com/gin-gonic/gin/pull/4068))
* refactor(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
* refactor(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
* tree: replace the self-defined 'min' to official one ([#3975](https://github.com/gin-gonic/gin/pull/3975))
* context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181))
### Bug Fixes
* fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987))
* fix(binding): prevent duplicate decoding and add validation in decodeToml ([#4193](https://github.com/gin-gonic/gin/pull/4193))
* fix(gin): Do not panic when handling method not allowed on empty tree ([#4003](https://github.com/gin-gonic/gin/pull/4003))
* fix(gin): data race warning for gin mode ([#1580](https://github.com/gin-gonic/gin/pull/1580))
* fix(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
* fix(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
* fix(context): check handler is nil ([#3413](https://github.com/gin-gonic/gin/pull/3413))
* fix(readme): fix broken link to English documentation ([#4222](https://github.com/gin-gonic/gin/pull/4222))
* fix(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077))
### Build process updates / CI
* ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359))
* ci: support Go 1.25 in CI/CD ([#4341](https://github.com/gin-gonic/gin/pull/4341))
* build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 ([#4342](https://github.com/gin-gonic/gin/pull/4342))
* ci: add Go version 1.24 to GitHub Actions ([#4154](https://github.com/gin-gonic/gin/pull/4154))
* build: update Gin minimum Go version to 1.21 ([#3960](https://github.com/gin-gonic/gin/pull/3960))
* ci(lint): enable new linters (testifylint, usestdlibvars, perfsprint, etc.) ([#4010](https://github.com/gin-gonic/gin/pull/4010), [#4091](https://github.com/gin-gonic/gin/pull/4091), [#4090](https://github.com/gin-gonic/gin/pull/4090))
* ci(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126))
### Dependency updates
* chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.9 ([#4346](https://github.com/gin-gonic/gin/pull/4346), [#4356](https://github.com/gin-gonic/gin/pull/4356))
* chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 ([#4347](https://github.com/gin-gonic/gin/pull/4347))
* chore(deps): bump actions/setup-go from 5 to 6 ([#4351](https://github.com/gin-gonic/gin/pull/4351))
* chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 ([#4328](https://github.com/gin-gonic/gin/pull/4328))
* chore(deps): bump golang.org/x/net from 0.33.0 to 0.38.0 ([#4178](https://github.com/gin-gonic/gin/pull/4178), [#4221](https://github.com/gin-gonic/gin/pull/4221))
* chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 ([#4052](https://github.com/gin-gonic/gin/pull/4052))
### Documentation updates
* docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360))
* docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md ([#4207](https://github.com/gin-gonic/gin/pull/4207))
* docs: update documentation and release notes for Gin v1.10.0 ([#3953](https://github.com/gin-gonic/gin/pull/3953))
* docs: fix typo in Gin Quick Start ([#3997](https://github.com/gin-gonic/gin/pull/3997))
* docs: fix comment and link issues ([#4205](https://github.com/gin-gonic/gin/pull/4205), [#3938](https://github.com/gin-gonic/gin/pull/3938))
* docs: fix route group example code ([#4020](https://github.com/gin-gonic/gin/pull/4020))
* docs(readme): add Portuguese documentation ([#4078](https://github.com/gin-gonic/gin/pull/4078))
* docs(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079))
---
## Gin v1.10.1
### Features
* refactor: strengthen HTTPS security and improve code organization
* feat(binding): Support custom BindUnmarshaler for binding. (#3933)
### Enhancements
* chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940)
* chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941)
* chore: update external dependencies to latest versions (#3950)
* chore: update various Go dependencies to latest versions (#3901)
* chore: refactor configuration files for better readability (#3951)
* chore: update changelog categories and improve documentation (#3917)
* feat: update version constant to v1.10.0 (#3952)
### Build process updates
* ci(release): refactor changelog regex patterns and exclusions (#3914)
* ci(Makefile): vet command add .PHONY (#3915)
## Gin v1.10.0 ## Gin v1.10.0
### Features ### Features

View File

@ -1,13 +1,41 @@
## Contributing # Contributing
- With issues: We welcome both issue reports and pull requests! Please follow these guidelines to help maintainers respond effectively.
- Use the search tool before opening a new issue.
- Please provide source code and commit sha if you found a bug. ## Issues
- **Before opening a new issue:**
- Use the search tool to check for existing issues or feature requests.
- Review existing issues and provide feedback or react to them. - Review existing issues and provide feedback or react to them.
- Use English for all communications — it is the language all maintainers read and write.
- For questions, configuration or deployment problems, please use the [Discussions Forum](https://github.com/gin-gonic/gin/discussions).
- For bug reports involving sensitive security issues, email <appleboy.tw@gmail.com> instead of posting publicly.
- With pull requests: - **Reporting a bug:**
- Open your pull request against `master` - Please provide a clear description of your issue, and a minimal reproducible code example if possible.
- Your pull request should have no more than two commits, if not you should squash them. - Include the Gin version (or commit reference), Go version, and operating system.
- It should pass all tests in the available continuous integration systems such as GitHub Actions. - Indicate whether you can reproduce the bug and describe steps to do so.
- You should add/modify tests to cover your proposed code changes. - Attach relevant logs per [Logging Documentation](https://docs.gitea.com/administration/logging-config#collecting-logs-for-help).
- If your pull request contains a new feature, please document it on the README.
- **Feature requests:**
- Before opening a request, check that a similar idea hasnt already been suggested.
- Clearly describe your proposed feature and its benefits.
_For API Documentation, User Guides, and more, see:_
- [Go.dev API Documentation](https://pkg.go.dev/github.com/gin-gonic/gin)
- [Gin User Guides](https://gin-gonic.com/)
- [Discussions Forum](https://github.com/gin-gonic/gin/discussions)
## Pull Requests
Please ensure your pull request meets the following requirements:
- Open your pull request against the `master` branch.
- Your pull request should have no more than two commits — squash them if necessary.
- All tests pass in available continuous integration systems (e.g., GitHub Actions).
- Add or modify tests to cover your code changes.
- If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README.
- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md:1).
Thank you for contributing!

189
README.md
View File

@ -3,116 +3,149 @@
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png"> <img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
[![Build Status](https://github.com/gin-gonic/gin/actions/workflows/gin.yml/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions/workflows/gin.yml) [![Build Status](https://github.com/gin-gonic/gin/actions/workflows/gin.yml/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
[![Trivy Security Scan](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc) [![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). ## 📰 [Announcing Gin 1.11.0!](https://gin-gonic.com/en/blog/news/gin-1-11-0-release-announcement/)
If you need performance and good productivity, you will love Gin.
**Gin's key features are:** Read about the latest features and improvements in Gin 1.11.0 on our official blog.
- Zero allocation router ---
- Speed
- Middleware support
- Crash-free
- JSON validation
- Route grouping
- Error management
- Built-in rendering
- Extensible
## Getting started Gin is a high-performance HTTP web framework written in [Go](https://go.dev/). It provides a Martini-like API but with significantly better performance—up to 40 times faster—thanks to [httprouter](https://github.com/julienschmidt/httprouter). Gin is designed for building REST APIs, web applications, and microservices where speed and developer productivity are essential.
**Why choose Gin?**
Gin combines the simplicity of Express.js-style routing with Go's performance characteristics, making it ideal for:
- Building high-throughput REST APIs
- Developing microservices that need to handle many concurrent requests
- Creating web applications that require fast response times
- Prototyping web services quickly with minimal boilerplate
**Gin's key features:**
- **Zero allocation router** - Extremely memory-efficient routing with no heap allocations
- **High performance** - Benchmarks show superior speed compared to other Go web frameworks
- **Middleware support** - Extensible middleware system for authentication, logging, CORS, etc.
- **Crash-free** - Built-in recovery middleware prevents panics from crashing your server
- **JSON validation** - Automatic request/response JSON binding and validation
- **Route grouping** - Organize related routes and apply common middleware
- **Error management** - Centralized error handling and logging
- **Built-in rendering** - Support for JSON, XML, HTML templates, and more
- **Extensible** - Large ecosystem of community middleware and plugins
## Getting Started
### Prerequisites ### Prerequisites
Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above. - **Go version**: Gin requires [Go](https://go.dev/) version [1.24](https://go.dev/doc/devel/release#go1.24.0) or above
- **Basic Go knowledge**: Familiarity with Go syntax and package management is helpful
### Getting Gin ### Installation
With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), `go [build|run|test]` automatically fetches the necessary dependencies when you add the import in your code: With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), simply import Gin in your code and Go will automatically fetch it during build:
```sh ```go
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
``` ```
Alternatively, use `go get`: ### Your First Gin Application
```sh Here's a complete example that demonstrates Gin's simplicity:
go get -u github.com/gin-gonic/gin
```
### Running Gin
A basic example:
```go ```go
package main package main
import ( import (
"log"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func main() { func main() {
// Create a Gin router with default middleware (logger and recovery)
r := gin.Default() r := gin.Default()
// Define a simple GET endpoint
r.GET("/ping", func(c *gin.Context) { r.GET("/ping", func(c *gin.Context) {
// Return JSON response
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "pong", "message": "pong",
}) })
}) })
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
// Start server on port 8080 (default)
// Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows)
if err := r.Run(); err != nil {
log.Fatalf("failed to run server: %v", err)
}
} }
``` ```
To run the code, use the `go run` command, like: **Running the application:**
```sh 1. Save the code above as `main.go`
go run example.go 2. Run the application:
```
Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response! ```sh
go run main.go
```
### See more examples 3. Open your browser and visit [`http://localhost:8080/ping`](http://localhost:8080/ping)
4. You should see: `{"message":"pong"}`
#### Quick Start **What this example demonstrates:**
Learn and practice with the [Gin Quick Start](docs/doc.md), which includes API examples and builds tag. - Creating a Gin router with default middleware
- Defining HTTP endpoints with simple handler functions
- Returning JSON responses
- Starting an HTTP server
#### Examples ### Next Steps
A number of ready-to-run examples demonstrating various use cases of Gin are available in the [Gin examples](https://github.com/gin-gonic/examples) repository. After running your first Gin application, explore these resources to learn more:
## Documentation #### 📚 Learning Resources
See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gin). - **[Gin Quick Start Guide](docs/doc.md)** - Comprehensive tutorial with API examples and build configurations
- **[Example Repository](https://github.com/gin-gonic/examples)** - Ready-to-run examples demonstrating various Gin use cases:
- REST API development
- Authentication & middleware
- File uploads and downloads
- WebSocket connections
- Template rendering
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages: ## 📖 Documentation
- [English](https://gin-gonic.com/en/docs/) ### API Reference
- [简体中文](https://gin-gonic.com/zh-cn/docs/)
- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
- [日本語](https://gin-gonic.com/ja/docs/)
- [Español](https://gin-gonic.com/es/docs/)
- [한국어](https://gin-gonic.com/ko-kr/docs/)
- [Turkish](https://gin-gonic.com/tr/docs/)
- [Persian](https://gin-gonic.com/fa/docs/)
- [Português](https://gin-gonic.com/pt/docs/)
- [Russian](https://gin-gonic.com/ru/docs/)
- [Indonesian](https://gin-gonic.com/id/docs/)
### Articles - **[Go.dev API Documentation](https://pkg.go.dev/github.com/gin-gonic/gin)** - Complete API reference with examples
- [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin) ### User Guides
## Benchmarks The comprehensive documentation is available on [gin-gonic.com](https://gin-gonic.com) in multiple languages:
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks](/BENCHMARKS.md). - [English](https://gin-gonic.com/en/docs/) | [简体中文](https://gin-gonic.com/zh-cn/docs/) | [繁體中文](https://gin-gonic.com/zh-tw/docs/)
- [日本語](https://gin-gonic.com/ja/docs/) | [한국어](https://gin-gonic.com/ko-kr/docs/) | [Español](https://gin-gonic.com/es/docs/)
- [Turkish](https://gin-gonic.com/tr/docs/) | [Persian](https://gin-gonic.com/fa/docs/) | [Português](https://gin-gonic.com/pt/docs/)
- [Russian](https://gin-gonic.com/ru/docs/) | [Indonesian](https://gin-gonic.com/id/docs/)
### Official Tutorials
- [Go.dev Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
## ⚡ Performance Benchmarks
Gin demonstrates exceptional performance compared to other Go web frameworks. It uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) for maximum efficiency. [View detailed benchmarks →](/BENCHMARKS.md)
**Gin vs. Other Go Frameworks** (GitHub API routing benchmark):
| Benchmark name | (1) | (2) | (3) | (4) | | Benchmark name | (1) | (2) | (3) | (4) |
| ------------------------------ | --------: | --------------: | -----------: | --------------: | | ------------------------------ | --------: | --------------: | -----------: | --------------: |
@ -152,23 +185,43 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- (3): Heap Memory (B/op), lower is better - (3): Heap Memory (B/op), lower is better
- (4): Average Allocations per Repetition (allocs/op), lower is better - (4): Average Allocations per Repetition (allocs/op), lower is better
## Middleware ## 🔌 Middleware Ecosystem
You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib) and [gin-gonic/contrib](https://github.com/gin-gonic/contrib). Gin has a rich ecosystem of middleware for common web development needs. Explore community-contributed middleware:
## Uses - **[gin-contrib](https://github.com/gin-contrib)** - Official middleware collection including:
- Authentication (JWT, Basic Auth, Sessions)
- CORS, Rate limiting, Compression
- Logging, Metrics, Tracing
- Static file serving, Template engines
- **[gin-gonic/contrib](https://github.com/gin-gonic/contrib)** - Additional community middleware
Here are some awesome projects that are using the [Gin](https://github.com/gin-gonic/gin) web framework. ## 🏢 Production Usage
- [gorush](https://github.com/appleboy/gorush): A push notification server. Gin powers many high-traffic applications and services in production:
- [fnproject](https://github.com/fnproject/fn): A container native, cloud agnostic serverless platform.
- [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Google TensorFlow.
- [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middleware.
- [picfit](https://github.com/thoas/picfit): An image resizing server.
- [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
## Contributing - **[gorush](https://github.com/appleboy/gorush)** - High-performance push notification server
- **[fnproject](https://github.com/fnproject/fn)** - Container-native, serverless platform
- **[photoprism](https://github.com/photoprism/photoprism)** - AI-powered personal photo management
- **[lura](https://github.com/luraproject/lura)** - Ultra-performant API Gateway framework
- **[picfit](https://github.com/thoas/picfit)** - Real-time image processing server
- **[dkron](https://github.com/distribworks/dkron)** - Distributed job scheduling system
Gin is the work of hundreds of contributors. We appreciate your help! ## 🤝 Contributing
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. Gin is the work of hundreds of contributors from around the world. We welcome and appreciate your contributions!
### How to Contribute
- 🐛 **Report bugs** - Help us identify and fix issues
- 💡 **Suggest features** - Share your ideas for improvements
- 📝 **Improve documentation** - Help make our docs clearer
- 🔧 **Submit code** - Fix bugs or implement new features
- 🧪 **Write tests** - Improve our test coverage
### Getting Started with Contributing
1. Check out our [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines
2. Join our community discussions and ask questions
**All contributions are valued and help make Gin better for everyone!**

View File

@ -87,7 +87,7 @@ func BenchmarkOneRouteString(B *testing.B) {
runRequest(B, router, http.MethodGet, "/text") runRequest(B, router, http.MethodGet, "/text")
} }
func BenchmarkManyRoutesFist(B *testing.B) { func BenchmarkManyRoutesFirst(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, http.MethodGet, "/ping")

View File

@ -18,9 +18,8 @@ func BenchmarkSliceValidationError(b *testing.B) {
} }
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ { for b.Loop() {
if len(e.Error()) == 0 { if len(e.Error()) == 0 {
b.Errorf("error") b.Errorf("error")
} }

View File

@ -7,6 +7,7 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"maps"
"mime/multipart" "mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
@ -230,9 +231,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
switch value.Kind() { switch value.Kind() {
case reflect.Slice: case reflect.Slice:
if !ok { if len(vs) == 0 {
vs = []string{opt.defaultValue} if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present // pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format") cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" { if cfTag == "" || cfTag == "multi" {
@ -250,9 +254,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return true, setSlice(vs, value, field) return true, setSlice(vs, value, field)
case reflect.Array: case reflect.Array:
if !ok { if len(vs) == 0 {
vs = []string{opt.defaultValue} if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present // pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format") cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" { if cfTag == "" || cfTag == "multi" {
@ -293,6 +300,11 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
} }
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
// If it is a string type, no spaces are removed, and the user data is not modified here
if value.Kind() != reflect.String {
val = strings.TrimSpace(val)
}
switch value.Kind() { switch value.Kind() {
case reflect.Int: case reflect.Int:
return setIntField(val, 0, value) return setIntField(val, 0, value)
@ -397,6 +409,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339 timeFormat = time.RFC3339
} }
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
switch tf := strings.ToLower(timeFormat); tf { switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixmilli", "unixmicro", "unixnano": case "unix", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64) tv, err := strconv.ParseInt(val, 10, 64)
@ -420,11 +437,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
return nil return nil
} }
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
l := time.Local l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC { if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC l = time.UTC
@ -468,6 +480,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
} }
func setTimeDuration(val string, value reflect.Value) error { func setTimeDuration(val string, value reflect.Value) error {
if val == "" {
val = "0"
}
d, err := time.ParseDuration(val) d, err := time.ParseDuration(val)
if err != nil { if err != nil {
return err return err
@ -489,9 +505,7 @@ func setFormMap(ptr any, form map[string][]string) error {
if !ok { if !ok {
return ErrConvertMapStringSlice return ErrConvertMapStringSlice
} }
for k, v := range form { maps.Copy(ptrMap, form)
ptrMap[k] = v
}
return nil return nil
} }

View File

@ -31,7 +31,7 @@ type structFull struct {
func BenchmarkMapFormFull(b *testing.B) { func BenchmarkMapFormFull(b *testing.B) {
var s structFull var s structFull
for i := 0; i < b.N; i++ { for b.Loop() {
err := mapForm(&s, form) err := mapForm(&s, form)
if err != nil { if err != nil {
b.Fatalf("Error on a form mapping") b.Fatalf("Error on a form mapping")
@ -54,7 +54,7 @@ type structName struct {
func BenchmarkMapFormName(b *testing.B) { func BenchmarkMapFormName(b *testing.B) {
var s structName var s structName
for i := 0; i < b.N; i++ { for b.Loop() {
err := mapForm(&s, form) err := mapForm(&s, form)
if err != nil { if err != nil {
b.Fatalf("Error on a form mapping") b.Fatalf("Error on a form mapping")

View File

@ -226,7 +226,35 @@ func TestMappingTime(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
type bindTestData struct {
need any
got any
in map[string][]string
}
func TestMappingTimeUnixNano(t *testing.T) {
type needFixUnixNanoEmpty struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
}
// ok
tests := []bindTestData{
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
}
func TestMappingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
type needFixDurationEmpty struct {
Duration time.Duration `form:"duration"`
}
var s struct { var s struct {
D time.Duration D time.Duration
} }
@ -236,6 +264,17 @@ func TestMappingTimeDuration(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D) assert.Equal(t, 5*time.Second, s.D)
// ok
tests := []bindTestData{
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
// error // error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err) require.Error(t, err)
@ -635,3 +674,83 @@ func TestMappingCustomArrayForm(t *testing.T) {
expected, _ := convertTo(val) expected, _ := convertTo(val)
assert.Equal(t, expected, s.FileData) assert.Equal(t, expected, s.FileData)
} }
func TestMappingEmptyValues(t *testing.T) {
t.Run("slice with default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present but empty
err = mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present with values
err = mappingByPtr(&s, formSource{"slice": {"1", "2", "3"}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.Slice)
})
t.Run("array with default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
// field present but empty
err = mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
})
t.Run("slice without default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int(nil), s.Slice)
})
t.Run("array without default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{0}, s.Array)
})
t.Run("slice with collection format", func(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi,default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:"slice_csv,default=1;2;3" collection_format:"csv"`
}
// field not present
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)
// field present but empty
err = mappingByPtr(&s, formSource{"slice_multi": {}, "slice_csv": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
})
}

View File

@ -10,6 +10,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"log" "log"
"maps"
"math" "math"
"mime/multipart" "mime/multipart"
"net" "net"
@ -38,6 +39,7 @@ const (
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2 MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML MIMETOML = binding.MIMETOML
MIMEPROTOBUF = binding.MIMEPROTOBUF
) )
// BodyBytesKey indicates a default body bytes key. // BodyBytesKey indicates a default body bytes key.
@ -53,6 +55,14 @@ 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
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
func safeInt8(n int) int8 {
if n > math.MaxInt8 {
return math.MaxInt8
}
return int8(n)
}
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct { type Context struct {
@ -133,11 +143,8 @@ func (c *Context) Copy() *Context {
cp.fullPath = c.fullPath cp.fullPath = c.fullPath
cKeys := c.Keys cKeys := c.Keys
cp.Keys = make(map[any]any, len(cKeys))
c.mu.RLock() c.mu.RLock()
for k, v := range cKeys { cp.Keys = maps.Clone(cKeys)
cp.Keys[k] = v
}
c.mu.RUnlock() c.mu.RUnlock()
cParams := c.Params cParams := c.Params
@ -190,7 +197,7 @@ func (c *Context) FullPath() string {
// See example in GitHub. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
for c.index < int8(len(c.handlers)) { for c.index < safeInt8(len(c.handlers)) {
if c.handlers[c.index] != nil { if c.handlers[c.index] != nil {
c.handlers[c.index](c) c.handlers[c.index](c)
} }
@ -275,7 +282,7 @@ func (c *Context) Error(err error) *Error {
/************************************/ /************************************/
// Set is used to store a new key/value pair exclusively for this context. // Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key any, value any) { func (c *Context) Set(key any, value any) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -311,165 +318,175 @@ func getTyped[T any](c *Context, key any) (res T) {
} }
// GetString returns the value associated with the key as a string. // GetString returns the value associated with the key as a string.
func (c *Context) GetString(key any) (s string) { func (c *Context) GetString(key any) string {
return getTyped[string](c, key) 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 any) (b bool) { func (c *Context) GetBool(key any) bool {
return getTyped[bool](c, key) return getTyped[bool](c, key)
} }
// 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 any) (i int) { func (c *Context) GetInt(key any) int {
return getTyped[int](c, key) return getTyped[int](c, key)
} }
// GetInt8 returns the value associated with the key as an integer 8. // GetInt8 returns the value associated with the key as an integer 8.
func (c *Context) GetInt8(key any) (i8 int8) { func (c *Context) GetInt8(key any) int8 {
return getTyped[int8](c, key) return getTyped[int8](c, key)
} }
// GetInt16 returns the value associated with the key as an integer 16. // GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key any) (i16 int16) { func (c *Context) GetInt16(key any) int16 {
return getTyped[int16](c, key) return getTyped[int16](c, key)
} }
// GetInt32 returns the value associated with the key as an integer 32. // GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key any) (i32 int32) { func (c *Context) GetInt32(key any) int32 {
return getTyped[int32](c, key) return getTyped[int32](c, key)
} }
// GetInt64 returns the value associated with the key as an integer 64. // GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key any) (i64 int64) { func (c *Context) GetInt64(key any) int64 {
return getTyped[int64](c, key) return getTyped[int64](c, key)
} }
// 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 any) (ui uint) { func (c *Context) GetUint(key any) uint {
return getTyped[uint](c, key) return getTyped[uint](c, key)
} }
// GetUint8 returns the value associated with the key as an unsigned integer 8. // GetUint8 returns the value associated with the key as an unsigned integer 8.
func (c *Context) GetUint8(key any) (ui8 uint8) { func (c *Context) GetUint8(key any) uint8 {
return getTyped[uint8](c, key) return getTyped[uint8](c, key)
} }
// GetUint16 returns the value associated with the key as an unsigned integer 16. // GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key any) (ui16 uint16) { func (c *Context) GetUint16(key any) uint16 {
return getTyped[uint16](c, key) return getTyped[uint16](c, key)
} }
// GetUint32 returns the value associated with the key as an unsigned integer 32. // GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key any) (ui32 uint32) { func (c *Context) GetUint32(key any) uint32 {
return getTyped[uint32](c, key) return getTyped[uint32](c, key)
} }
// GetUint64 returns the value associated with the key as an unsigned integer 64. // GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key any) (ui64 uint64) { func (c *Context) GetUint64(key any) uint64 {
return getTyped[uint64](c, key) return getTyped[uint64](c, key)
} }
// GetFloat32 returns the value associated with the key as a float32. // GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key any) (f32 float32) { func (c *Context) GetFloat32(key any) float32 {
return getTyped[float32](c, key) 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 any) (f64 float64) { func (c *Context) GetFloat64(key any) float64 {
return getTyped[float64](c, key) return getTyped[float64](c, key)
} }
// 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 any) (t time.Time) { func (c *Context) GetTime(key any) time.Time {
return getTyped[time.Time](c, key) return getTyped[time.Time](c, key)
} }
// 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 any) (d time.Duration) { func (c *Context) GetDuration(key any) time.Duration {
return getTyped[time.Duration](c, key) return getTyped[time.Duration](c, key)
} }
// GetIntSlice returns the value associated with the key as a slice of integers. // GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) (is []int) { func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key) return getTyped[[]int](c, key)
} }
// GetInt8Slice returns the value associated with the key as a slice of int8 integers. // GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key any) (i8s []int8) { func (c *Context) GetInt8Slice(key any) []int8 {
return getTyped[[]int8](c, key) return getTyped[[]int8](c, key)
} }
// GetInt16Slice returns the value associated with the key as a slice of int16 integers. // GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key any) (i16s []int16) { func (c *Context) GetInt16Slice(key any) []int16 {
return getTyped[[]int16](c, key) return getTyped[[]int16](c, key)
} }
// GetInt32Slice returns the value associated with the key as a slice of int32 integers. // GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key any) (i32s []int32) { func (c *Context) GetInt32Slice(key any) []int32 {
return getTyped[[]int32](c, key) return getTyped[[]int32](c, key)
} }
// GetInt64Slice returns the value associated with the key as a slice of int64 integers. // GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key any) (i64s []int64) { func (c *Context) GetInt64Slice(key any) []int64 {
return getTyped[[]int64](c, key) return getTyped[[]int64](c, key)
} }
// GetUintSlice returns the value associated with the key as a slice of unsigned integers. // GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key any) (uis []uint) { func (c *Context) GetUintSlice(key any) []uint {
return getTyped[[]uint](c, key) return getTyped[[]uint](c, key)
} }
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers. // GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key any) (ui8s []uint8) { func (c *Context) GetUint8Slice(key any) []uint8 {
return getTyped[[]uint8](c, key) return getTyped[[]uint8](c, key)
} }
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers. // GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key any) (ui16s []uint16) { func (c *Context) GetUint16Slice(key any) []uint16 {
return getTyped[[]uint16](c, key) return getTyped[[]uint16](c, key)
} }
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers. // GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key any) (ui32s []uint32) { func (c *Context) GetUint32Slice(key any) []uint32 {
return getTyped[[]uint32](c, key) return getTyped[[]uint32](c, key)
} }
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers. // GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key any) (ui64s []uint64) { func (c *Context) GetUint64Slice(key any) []uint64 {
return getTyped[[]uint64](c, key) return getTyped[[]uint64](c, key)
} }
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers. // GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key any) (f32s []float32) { func (c *Context) GetFloat32Slice(key any) []float32 {
return getTyped[[]float32](c, key) return getTyped[[]float32](c, key)
} }
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers. // GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key any) (f64s []float64) { func (c *Context) GetFloat64Slice(key any) []float64 {
return getTyped[[]float64](c, key) 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 any) (ss []string) { func (c *Context) GetStringSlice(key any) []string {
return getTyped[[]string](c, key) return getTyped[[]string](c, key)
} }
// 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 any) (sm map[string]any) { func (c *Context) GetStringMap(key any) map[string]any {
return getTyped[map[string]any](c, key) return getTyped[map[string]any](c, key)
} }
// 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 any) (sms map[string]string) { func (c *Context) GetStringMapString(key any) map[string]string {
return getTyped[map[string]string](c, key) return getTyped[map[string]string](c, key)
} }
// 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 any) (smss map[string][]string) { func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
return getTyped[map[string][]string](c, key) return getTyped[map[string][]string](c, key)
} }
// Delete deletes the key from the Context's Key map, if it exists.
// This operation is safe to be used by concurrent go-routines
func (c *Context) Delete(key any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys != nil {
delete(c.Keys, key)
}
}
/************************************/ /************************************/
/************ INPUT DATA ************/ /************ INPUT DATA ************/
/************************************/ /************************************/
@ -657,14 +674,23 @@ func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) { func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
d := make(map[string]string) d := make(map[string]string)
found := false found := false
keyLen := len(key)
for k, v := range m { for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key { if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 { continue
found = true }
d[k[i+1:][:j]] = v[0]
} if k[:keyLen] != key || k[keyLen] != '[' {
continue
}
if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 {
found = true
d[k[keyLen+1:keyLen+1+j]] = v[0]
} }
} }
return d, found return d, found
} }
@ -815,41 +841,71 @@ func (c *Context) ShouldBind(obj any) error {
} }
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON). // ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
//
// Example:
//
// POST /user
// Content-Type: application/json
//
// Request Body:
// {
// "name": "Manu",
// "age": 20
// }
//
// type User struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// var user User
// if err := c.ShouldBindJSON(&user); err != nil {
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// return
// }
// c.JSON(http.StatusOK, user)
func (c *Context) ShouldBindJSON(obj any) error { func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON) return c.ShouldBindWith(obj, binding.JSON)
} }
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). // ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
// It works like ShouldBindJSON but binds the request body as XML data.
func (c *Context) ShouldBindXML(obj any) error { func (c *Context) ShouldBindXML(obj any) error {
return c.ShouldBindWith(obj, binding.XML) return c.ShouldBindWith(obj, binding.XML)
} }
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
// It works like ShouldBindJSON but binds query parameters from the URL.
func (c *Context) ShouldBindQuery(obj any) error { func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query) return c.ShouldBindWith(obj, binding.Query)
} }
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). // ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
// It works like ShouldBindJSON but binds the request body as YAML data.
func (c *Context) ShouldBindYAML(obj any) error { func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML) return c.ShouldBindWith(obj, binding.YAML)
} }
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML). // ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
// It works like ShouldBindJSON but binds the request body as TOML data.
func (c *Context) ShouldBindTOML(obj any) error { func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML) return c.ShouldBindWith(obj, binding.TOML)
} }
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain). // ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
// It works like ShouldBindJSON but binds plain text data from the request body.
func (c *Context) ShouldBindPlain(obj any) error { func (c *Context) ShouldBindPlain(obj any) error {
return c.ShouldBindWith(obj, binding.Plain) 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).
// It works like ShouldBindJSON but binds values from HTTP headers.
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)
} }
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
// It works like ShouldBindJSON but binds parameters from the URI.
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, len(c.Params))
for _, v := range c.Params { for _, v := range c.Params {
@ -1270,14 +1326,15 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
// Negotiate contains all negotiations data. // Negotiate contains all negotiations data.
type Negotiate struct { type Negotiate struct {
Offered []string Offered []string
HTMLName string HTMLName string
HTMLData any HTMLData any
JSONData any JSONData any
XMLData any XMLData any
YAMLData any YAMLData any
Data any Data any
TOMLData any TOMLData any
PROTOBUFData any
} }
// Negotiate calls different Render according to acceptable Accept format. // Negotiate calls different Render according to acceptable Accept format.
@ -1303,6 +1360,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.TOMLData, config.Data) data := chooseData(config.TOMLData, config.Data)
c.TOML(code, data) c.TOML(code, data)
case binding.MIMEPROTOBUF:
data := chooseData(config.PROTOBUFData, config.Data)
c.ProtoBuf(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
} }

View File

@ -292,7 +292,7 @@ func TestContextReset(t *testing.T) {
assert.Empty(t, c.Errors.Errors()) assert.Empty(t, c.Errors.Errors())
assert.Empty(t, c.Errors.ByType(ErrorTypeAny)) assert.Empty(t, c.Errors.ByType(ErrorTypeAny))
assert.Empty(t, c.Params) assert.Empty(t, c.Params)
assert.EqualValues(t, c.index, -1) assert.EqualValues(t, -1, c.index)
assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
} }
@ -384,7 +384,7 @@ func TestContextSetGetValues(t *testing.T) {
c.Set("intInterface", a) c.Set("intInterface", a)
assert.Exactly(t, "this is a string", c.MustGet("string").(string)) assert.Exactly(t, "this is a string", c.MustGet("string").(string))
assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) assert.Exactly(t, int32(-42), c.MustGet("int32").(int32))
assert.Exactly(t, int64(42424242424242), c.MustGet("int64").(int64)) assert.Exactly(t, int64(42424242424242), c.MustGet("int64").(int64))
assert.Exactly(t, uint64(42), c.MustGet("uint64").(uint64)) assert.Exactly(t, uint64(42), c.MustGet("uint64").(uint64))
assert.InDelta(t, float32(4.2), c.MustGet("float32").(float32), 0.01) assert.InDelta(t, float32(4.2), c.MustGet("float32").(float32), 0.01)
@ -404,6 +404,19 @@ func TestContextSetGetBool(t *testing.T) {
assert.True(t, c.GetBool("bool")) assert.True(t, c.GetBool("bool"))
} }
func TestSetGetDelete(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "example-key"
value := "example-value"
c.Set(key, value)
val, exists := c.Get(key)
assert.True(t, exists)
assert.Equal(t, val, value)
c.Delete(key)
_, exists = c.Get(key)
assert.False(t, exists)
}
func TestContextGetInt(t *testing.T) { func TestContextGetInt(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("int", 1) c.Set("int", 1)
@ -1233,7 +1246,7 @@ func TestContextRenderNoContentHTML(t *testing.T) {
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"))
} }
// TestContextXML tests that the response is serialized as XML // TestContextRenderXML tests that the response is serialized as XML
// and Content-Type is set to application/xml // and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) { func TestContextRenderXML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -1615,6 +1628,32 @@ func TestContextNegotiationWithHTML(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestContextNegotiationWithPROTOBUF(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}
c.Negotiate(http.StatusCreated, Negotiate{
Offered: []string{MIMEPROTOBUF, MIMEJSON, MIMEXML},
Data: data,
})
// Marshal original data for comparison
protoData, err := proto.Marshal(data)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
func TestContextNegotiationNotSupport(t *testing.T) { func TestContextNegotiationNotSupport(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1991,13 +2030,12 @@ func TestContextContentType(t *testing.T) {
} }
func TestContextBindRequestTooLarge(t *testing.T) { func TestContextBindRequestTooLarge(t *testing.T) {
// When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error // When using go-json as JSON encoder, they do not propagate the http.MaxBytesError error
// The response will fail with a generic 400 instead of 413 // The response will fail with a generic 400 instead of 413
// https://github.com/goccy/go-json/issues/485 // https://github.com/goccy/go-json/issues/485
// https://github.com/bytedance/sonic/issues/800
var expectedCode int var expectedCode int
switch json.Package { switch json.Package {
case "github.com/goccy/go-json", "github.com/bytedance/sonic": case "github.com/goccy/go-json":
expectedCode = http.StatusBadRequest expectedCode = http.StatusBadRequest
default: default:
expectedCode = http.StatusRequestEntityTooLarge expectedCode = http.StatusRequestEntityTooLarge
@ -3321,7 +3359,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Max-Age=1") assert.Contains(t, setCookie, "Max-Age=1")
assert.Contains(t, setCookie, "HttpOnly") assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure") assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default // SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax") // assert.Contains(t, setCookie, "SameSite=Lax")
// Test that when Path is empty, "/" is automatically set // Test that when Path is empty, "/" is automatically set
@ -3342,7 +3380,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Max-Age=1") assert.Contains(t, setCookie, "Max-Age=1")
assert.Contains(t, setCookie, "HttpOnly") assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure") assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default // SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax") // assert.Contains(t, setCookie, "SameSite=Lax")
// Test additional cookie attributes (Expires) // Test additional cookie attributes (Expires)
@ -3365,7 +3403,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Domain=localhost") assert.Contains(t, setCookie, "Domain=localhost")
assert.Contains(t, setCookie, "HttpOnly") assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure") assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default // SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax") // assert.Contains(t, setCookie, "SameSite=Lax")
// Test for Partitioned attribute (Go 1.18+) // Test for Partitioned attribute (Go 1.18+)
@ -3385,7 +3423,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Domain=localhost") assert.Contains(t, setCookie, "Domain=localhost")
assert.Contains(t, setCookie, "HttpOnly") assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure") assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default // SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax") // assert.Contains(t, setCookie, "SameSite=Lax")
// Not testing for Partitioned attribute as it may not be supported in all Go versions // Not testing for Partitioned attribute as it may not be supported in all Go versions
@ -3596,3 +3634,88 @@ func TestGetMapFromFormData(t *testing.T) {
}) })
} }
} }
func BenchmarkGetMapFromFormData(b *testing.B) {
// Test case 1: Small dataset with bracket notation
smallData := map[string][]string{
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
"names[a]": {"mike"},
"names[b]": {"maria"},
}
// Test case 2: Medium dataset with mixed data
mediumData := map[string][]string{
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
"ids[c]": {"test"},
"ids[d]": {"value"},
"names[a]": {"mike"},
"names[b]": {"maria"},
"names[c]": {"john"},
"names[d]": {"jane"},
"other[key1]": {"value1"},
"other[key2]": {"value2"},
"simple": {"data"},
"another": {"info"},
}
// Test case 3: Large dataset with many bracket keys
largeData := make(map[string][]string)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("ids[%d]", i)
largeData[key] = []string{fmt.Sprintf("value%d", i)}
}
for i := 0; i < 50; i++ {
key := fmt.Sprintf("names[%d]", i)
largeData[key] = []string{fmt.Sprintf("name%d", i)}
}
for i := 0; i < 25; i++ {
key := fmt.Sprintf("other[key%d]", i)
largeData[key] = []string{fmt.Sprintf("other%d", i)}
}
// Test case 4: Dataset with many non-matching keys (worst case)
worstCaseData := make(map[string][]string)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("nonmatching%d", i)
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
}
worstCaseData["ids[a]"] = []string{"hi"}
worstCaseData["ids[b]"] = []string{"3.14"}
// Test case 5: Dataset with short keys (best case for early exit)
shortKeysData := map[string][]string{
"a": {"value1"},
"b": {"value2"},
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
}
benchmarks := []struct {
name string
data map[string][]string
key string
}{
{"Small_Bracket", smallData, "ids"},
{"Small_Names", smallData, "names"},
{"Medium_Bracket", mediumData, "ids"},
{"Medium_Names", mediumData, "names"},
{"Medium_Other", mediumData, "other"},
{"Large_Bracket", largeData, "ids"},
{"Large_Names", largeData, "names"},
{"Large_Other", largeData, "other"},
{"WorstCase_Bracket", worstCaseData, "ids"},
{"ShortKeys_Bracket", shortKeysData, "ids"},
{"Empty_Key", smallData, "notfound"},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = getMapFromFormData(bm.data, bm.key)
}
})
}
}

View File

@ -13,7 +13,7 @@ import (
"sync/atomic" "sync/atomic"
) )
const ginSupportMinGoVer = 23 const ginSupportMinGoVer = 24
// 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.
@ -78,7 +78,7 @@ 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.24+.
`) `)
} }

View File

@ -106,7 +106,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
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.24+.\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)
} }

76
gin.go
View File

@ -11,7 +11,6 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"regexp"
"strings" "strings"
"sync" "sync"
@ -23,10 +22,12 @@ import (
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
) )
const defaultMultipartMemory = 32 << 20 // 32 MB const (
const escapedColon = "\\:" defaultMultipartMemory = 32 << 20 // 32 MB
const colon = ":" escapedColon = "\\:"
const backslash = "\\" colon = ":"
backslash = "\\"
)
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
@ -46,9 +47,6 @@ 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)
@ -94,6 +92,10 @@ const (
type Engine struct { type Engine struct {
RouterGroup RouterGroup
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
routeTreesUpdated sync.Once
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a // RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists. // handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the // For example if /foo/ is requested but a route only exists for /foo, the
@ -133,10 +135,16 @@ type Engine struct {
AppEngine bool AppEngine bool
// UseRawPath if enabled, the url.RawPath will be used to find parameters. // UseRawPath if enabled, the url.RawPath will be used to find parameters.
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
// Only use RawPath if you know what you are doing.
UseRawPath bool UseRawPath bool
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
// It overrides UseRawPath
UseEscapedPath bool
// UnescapePathValues if true, the path value will be unescaped. // UnescapePathValues if true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true, // If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped. // as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool UnescapePathValues bool
@ -189,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
// - HandleMethodNotAllowed: false // - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true // - ForwardedByClientIP: true
// - UseRawPath: false // - UseRawPath: false
// - UseEscapedPath: false
// - UnescapePathValues: true // - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine { func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
@ -206,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform, TrustedPlatform: defaultPlatform,
UseRawPath: false, UseRawPath: false,
UseEscapedPath: false,
RemoveExtraSlash: false, RemoveExtraSlash: false,
UnescapePathValues: true, UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory, MaxMultipartMemory: defaultMultipartMemory,
@ -537,7 +547,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
engine.updateRouteTrees() engine.updateRouteTrees()
address := resolveAddress(addr) address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address) debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler()) server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
err = server.ListenAndServe()
return return
} }
@ -553,7 +567,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
} }
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler()) server := &http.Server{ // #nosec G112
Addr: addr,
Handler: engine.Handler(),
}
err = server.ListenAndServeTLS(certFile, keyFile)
return return
} }
@ -576,7 +594,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
defer listener.Close() defer listener.Close()
defer os.Remove(file) defer os.Remove(file)
err = http.Serve(listener, engine.Handler()) server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
err = server.Serve(listener)
return return
} }
@ -593,6 +614,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
} }
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd)) f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
defer f.Close()
listener, err := net.FileListener(f) listener, err := net.FileListener(f)
if err != nil { if err != nil {
return return
@ -629,12 +651,19 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
} }
err = http.Serve(listener, engine.Handler()) server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
err = server.Serve(listener)
return return
} }
// ServeHTTP conforms to the http.Handler interface. // ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.routeTreesUpdated.Do(func() {
engine.updateRouteTrees()
})
c := engine.pool.Get().(*Context) c := engine.pool.Get().(*Context)
c.writermem.reset(w) c.writermem.reset(w)
c.Request = req c.Request = req
@ -662,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method httpMethod := c.Request.Method
rPath := c.Request.URL.Path rPath := c.Request.URL.Path
unescape := false unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
if engine.UseEscapedPath {
rPath = c.Request.URL.EscapedPath()
unescape = engine.UnescapePathValues
} else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} }
@ -749,8 +782,8 @@ 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 = sanitizePathChars(prefix)
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/") prefix = removeRepeatedChar(prefix, '/')
p = prefix + "/" + req.URL.Path p = prefix + "/" + req.URL.Path
} }
@ -761,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
redirectRequest(c) redirectRequest(c)
} }
// sanitizePathChars removes unsafe characters from path strings,
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
func sanitizePathChars(s string) string {
return strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
return r
}
return -1
}, s)
}
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request req := c.Request
rPath := req.URL.Path rPath := req.URL.Path

View File

@ -12,17 +12,9 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var ( var engine = sync.OnceValue(func() *gin.Engine {
once sync.Once return gin.Default()
internalEngine *gin.Engine })
)
func engine() *gin.Engine {
once.Do(func() {
internalEngine = gin.Default()
})
return internalEngine
}
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob. // LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
func LoadHTMLGlob(pattern string) { func LoadHTMLGlob(pattern string) {

246
ginS/gins_test.go Normal file
View File

@ -0,0 +1,246 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package ginS
import (
"html/template"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func init() {
gin.SetMode(gin.TestMode)
}
func TestGET(t *testing.T) {
GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "test")
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "test", w.Body.String())
}
func TestPOST(t *testing.T) {
POST("/post", func(c *gin.Context) {
c.String(http.StatusCreated, "created")
})
req := httptest.NewRequest(http.MethodPost, "/post", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "created", w.Body.String())
}
func TestPUT(t *testing.T) {
PUT("/put", func(c *gin.Context) {
c.String(http.StatusOK, "updated")
})
req := httptest.NewRequest(http.MethodPut, "/put", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "updated", w.Body.String())
}
func TestDELETE(t *testing.T) {
DELETE("/delete", func(c *gin.Context) {
c.String(http.StatusOK, "deleted")
})
req := httptest.NewRequest(http.MethodDelete, "/delete", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "deleted", w.Body.String())
}
func TestPATCH(t *testing.T) {
PATCH("/patch", func(c *gin.Context) {
c.String(http.StatusOK, "patched")
})
req := httptest.NewRequest(http.MethodPatch, "/patch", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "patched", w.Body.String())
}
func TestOPTIONS(t *testing.T) {
OPTIONS("/options", func(c *gin.Context) {
c.String(http.StatusOK, "options")
})
req := httptest.NewRequest(http.MethodOptions, "/options", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "options", w.Body.String())
}
func TestHEAD(t *testing.T) {
HEAD("/head", func(c *gin.Context) {
c.String(http.StatusOK, "head")
})
req := httptest.NewRequest(http.MethodHead, "/head", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestAny(t *testing.T) {
Any("/any", func(c *gin.Context) {
c.String(http.StatusOK, "any")
})
req := httptest.NewRequest(http.MethodGet, "/any", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "any", w.Body.String())
}
func TestHandle(t *testing.T) {
Handle(http.MethodGet, "/handle", func(c *gin.Context) {
c.String(http.StatusOK, "handle")
})
req := httptest.NewRequest(http.MethodGet, "/handle", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "handle", w.Body.String())
}
func TestGroup(t *testing.T) {
group := Group("/group")
group.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "group test")
})
req := httptest.NewRequest(http.MethodGet, "/group/test", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "group test", w.Body.String())
}
func TestUse(t *testing.T) {
var middlewareExecuted bool
Use(func(c *gin.Context) {
middlewareExecuted = true
c.Next()
})
GET("/middleware-test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/middleware-test", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.True(t, middlewareExecuted)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestNoRoute(t *testing.T) {
NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "custom 404")
})
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "custom 404", w.Body.String())
}
func TestNoMethod(t *testing.T) {
NoMethod(func(c *gin.Context) {
c.String(http.StatusMethodNotAllowed, "method not allowed")
})
// This just verifies that NoMethod is callable
// Testing the actual behavior would require a separate engine instance
assert.NotNil(t, engine())
}
func TestRoutes(t *testing.T) {
GET("/routes-test", func(c *gin.Context) {})
routes := Routes()
assert.NotEmpty(t, routes)
found := false
for _, route := range routes {
if route.Path == "/routes-test" && route.Method == http.MethodGet {
found = true
break
}
}
assert.True(t, found)
}
func TestSetHTMLTemplate(t *testing.T) {
tmpl := template.Must(template.New("test").Parse("Hello {{.}}"))
SetHTMLTemplate(tmpl)
// Verify engine has template set
assert.NotNil(t, engine())
}
func TestStaticFile(t *testing.T) {
StaticFile("/static-file", "../testdata/test_file.txt")
req := httptest.NewRequest(http.MethodGet, "/static-file", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestStatic(t *testing.T) {
Static("/static-dir", "../testdata")
req := httptest.NewRequest(http.MethodGet, "/static-dir/test_file.txt", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestStaticFS(t *testing.T) {
fs := http.Dir("../testdata")
StaticFS("/static-fs", fs)
req := httptest.NewRequest(http.MethodGet, "/static-fs/test_file.txt", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}

View File

@ -16,6 +16,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -69,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
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.Run()) assert.NoError(t, router.Run())
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // Wait for server to be ready with exponential backoff
time.Sleep(5 * time.Millisecond) err := waitForServerReady("http://localhost:8080/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080")) require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
@ -212,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
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.Run()) assert.NoError(t, router.Run())
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // Wait for server to be ready with exponential backoff
time.Sleep(5 * time.Millisecond) err := waitForServerReady("http://localhost:3123/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":3123")) require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example") testRequest(t, "http://localhost:3123/example")
@ -233,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
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.Run(":5150")) assert.NoError(t, router.Run(":5150"))
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // Wait for server to be ready with exponential backoff
time.Sleep(5 * time.Millisecond) err := waitForServerReady("http://localhost:5150/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":5150")) require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example") testRequest(t, "http://localhost:5150/example")
@ -261,10 +265,11 @@ func TestUnixSocket(t *testing.T) {
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)
var response string var responseBuilder strings.Builder
for scanner.Scan() { for scanner.Scan() {
response += scanner.Text() responseBuilder.WriteString(scanner.Text())
} }
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match") assert.Contains(t, response, "it worked", "resp body should match")
} }
@ -322,10 +327,11 @@ func TestFileDescriptor(t *testing.T) {
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)
var response string var responseBuilder strings.Builder
for scanner.Scan() { for scanner.Scan() {
response += scanner.Text() responseBuilder.WriteString(scanner.Text())
} }
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match") assert.Contains(t, response, "it worked", "resp body should match")
} }
@ -354,10 +360,11 @@ func TestListener(t *testing.T) {
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)
var response string var responseBuilder strings.Builder
for scanner.Scan() { for scanner.Scan() {
response += scanner.Text() responseBuilder.WriteString(scanner.Text())
} }
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match") assert.Contains(t, response, "it worked", "resp body should match")
} }

View File

@ -545,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
} }
func TestRebuild404Handlers(t *testing.T) { func TestRebuild404Handlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
router := New()
// Initially, allNoRoute should be nil
assert.Nil(t, router.allNoRoute)
// Set NoRoute handlers
router.NoRoute(middleware0)
assert.Len(t, router.allNoRoute, 1)
assert.Len(t, router.noRoute, 1)
compareFunc(t, router.allNoRoute[0], middleware0)
// Add Use middleware should trigger rebuild404Handlers
router.Use(middleware1)
assert.Len(t, router.allNoRoute, 2)
assert.Len(t, router.Handlers, 1)
assert.Len(t, router.noRoute, 1)
// Global middleware should come first
compareFunc(t, router.allNoRoute[0], middleware1)
compareFunc(t, router.allNoRoute[1], middleware0)
} }
func TestNoMethodWithGlobalHandlers(t *testing.T) { func TestNoMethodWithGlobalHandlers(t *testing.T) {
@ -720,6 +743,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
assert.Equal(t, int64(1), handlerCounterV2) assert.Equal(t, int64(1), handlerCounterV2)
} }
func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) {
r := New()
r.UseEscapedPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
// Path is Escaped, the %25 is not interpreted as %
assert.Equal(t, "foo%252Fbar", c.Param("path"))
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) {
r := New()
r.UseRawPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
// Path is used, the %25 is interpreted as %
assert.Equal(t, "foo%2Fbar", c.Param("path"))
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) {
r := New()
r.UseEscapedPath = true
r.UseRawPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
assert.Equal(t, "foo%25bar", c.Param("path"))
c.Status(http.StatusOK)
})
assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar")
assert.Equal(t, 200, w.Code)
})
}
func TestPrepareTrustedCIRDsWith(t *testing.T) { func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New() r := New()
@ -913,3 +985,102 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) }) assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code) assert.Equal(t, http.StatusNotFound, resp.Code)
} }
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
func TestLiteralColonWithRun(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.updateRouteTrees()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithDirectServeHTTP(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHandler(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
handler := router.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHTTPServer(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.GET("/test/:param", func(c *Context) {
c.JSON(http.StatusOK, H{"param": c.Param("param")})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil)
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
assert.Contains(t, w2.Body.String(), "foo")
}
// Test that updateRouteTrees is called only once
func TestUpdateRouteTreesCalledOnce(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.String(http.StatusOK, "ok")
})
for range 5 {
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ok", w.Body.String())
}
}

42
go.mod
View File

@ -1,44 +1,42 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.23.0 go 1.24.0
require ( require (
github.com/bytedance/sonic v1.13.2 github.com/bytedance/sonic v1.14.2
github.com/gin-contrib/sse v1.1.0 github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.27.0 github.com/go-playground/validator/v10 v10.28.0
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/goccy/go-yaml v1.18.0 github.com/goccy/go-yaml v1.19.0
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.20
github.com/modern-go/reflect2 v1.0.2 github.com/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.54.0 github.com/quic-go/quic-go v0.57.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.0 github.com/ugorji/go/codec v1.3.1
golang.org/x/net v0.42.0 golang.org/x/net v0.47.0
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.10
) )
require ( require (
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // 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.8 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // 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/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 github.com/quic-go/qpack v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/crypto v0.40.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/mod v0.25.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

99
go.sum
View File

@ -1,16 +1,17 @@
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -19,20 +20,23 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -45,48 +49,47 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
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.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.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -41,6 +41,15 @@ func TestBytesToString(t *testing.T) {
} }
} }
func TestBytesToStringEmpty(t *testing.T) {
if got := BytesToString([]byte{}); got != "" {
t.Fatalf("BytesToString([]byte{}) = %q; want empty string", got)
}
if got := BytesToString(nil); got != "" {
t.Fatalf("BytesToString(nil) = %q; want empty string", got)
}
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const ( const (
letterIdxBits = 6 // 6 bits to represent a letter index letterIdxBits = 6 // 6 bits to represent a letter index
@ -78,28 +87,38 @@ func TestStringToBytes(t *testing.T) {
} }
} }
func TestStringToBytesEmpty(t *testing.T) {
b := StringToBytes("")
if len(b) != 0 {
t.Fatalf(`StringToBytes("") length = %d; want 0`, len(b))
}
if !bytes.Equal(b, []byte("")) {
t.Fatalf(`StringToBytes("") = %v; want []byte("")`, b)
}
}
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true // go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) { func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
rawBytesToStr(testBytes) rawBytesToStr(testBytes)
} }
} }
func BenchmarkBytesConvBytesToStr(b *testing.B) { func BenchmarkBytesConvBytesToStr(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
BytesToString(testBytes) BytesToString(testBytes)
} }
} }
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) { func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
rawStrToBytes(testString) rawStrToBytes(testString)
} }
} }
func BenchmarkBytesConvStrToBytes(b *testing.B) { func BenchmarkBytesConvStrToBytes(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
StringToBytes(testString) StringToBytes(testString)
} }
} }

View File

@ -103,6 +103,27 @@ func (p *LogFormatterParams) StatusCodeColor() string {
} }
} }
// LatencyColor is the ANSI color for latency
func (p *LogFormatterParams) LatencyColor() string {
latency := p.Latency
switch {
case latency < time.Millisecond*100:
return white
case latency < time.Millisecond*200:
return green
case latency < time.Millisecond*300:
return cyan
case latency < time.Millisecond*500:
return blue
case latency < time.Second:
return yellow
case latency < time.Second*2:
return magenta
default:
return red
}
}
// MethodColor is the ANSI color for appropriately logging http method to a terminal. // MethodColor is the ANSI color for appropriately logging http method to a terminal.
func (p *LogFormatterParams) MethodColor() string { func (p *LogFormatterParams) MethodColor() string {
method := p.Method method := p.Method
@ -139,20 +160,27 @@ func (p *LogFormatterParams) IsOutputColor() bool {
// defaultLogFormatter is the default log format function Logger middleware uses. // defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string { var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string var statusColor, methodColor, resetColor, latencyColor string
if param.IsOutputColor() { if param.IsOutputColor() {
statusColor = param.StatusCodeColor() statusColor = param.StatusCodeColor()
methodColor = param.MethodColor() methodColor = param.MethodColor()
resetColor = param.ResetColor() resetColor = param.ResetColor()
latencyColor = param.LatencyColor()
} }
if param.Latency > time.Minute { switch {
param.Latency = param.Latency.Truncate(time.Second) case param.Latency > time.Minute:
param.Latency = param.Latency.Truncate(time.Second * 10)
case param.Latency > time.Second:
param.Latency = param.Latency.Truncate(time.Millisecond * 10)
case param.Latency > time.Millisecond:
param.Latency = param.Latency.Truncate(time.Microsecond * 10)
} }
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
return fmt.Sprintf("[GIN] %v |%s %3d %s|%s %8v %s| %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor, statusColor, param.StatusCode, resetColor,
param.Latency, latencyColor, param.Latency, resetColor,
param.ClientIP, param.ClientIP,
methodColor, param.Method, resetColor, methodColor, param.Method, resetColor,
param.Path, param.Path,

View File

@ -39,7 +39,7 @@ func TestLogger(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more // I wrote these first (extending the above) but then realized they are more
// 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. I'm not sure where these should go.
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodPost, "/example") PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
@ -103,7 +103,7 @@ func TestLoggerWithConfig(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more // I wrote these first (extending the above) but then realized they are more
// 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. I'm not sure where these should go.
buffer.Reset() buffer.Reset()
PerformRequest(router, http.MethodPost, "/example") PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
@ -277,11 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
isTerm: false, isTerm: false,
} }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m0s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 5s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
@ -317,6 +317,23 @@ func TestColorForStatus(t *testing.T) {
assert.Equal(t, red, colorForStatus(2), "other things should be red") assert.Equal(t, red, colorForStatus(2), "other things should be red")
} }
func TestColorForLatency(t *testing.T) {
colorForLantency := func(latency time.Duration) string {
p := LogFormatterParams{
Latency: latency,
}
return p.LatencyColor()
}
assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white")
assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white")
assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green")
assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan")
assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow")
assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta")
assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red")
}
func TestResetColor(t *testing.T) { func TestResetColor(t *testing.T) {
p := LogFormatterParams{} p := LogFormatterParams{}
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor()) assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())

55
path.go
View File

@ -5,6 +5,8 @@
package gin package gin
const stackBufSize = 128
// cleanPath is the URL version of path.Clean, it returns a canonical URL path // cleanPath is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements. // for p, eliminating . and .. elements.
// //
@ -19,7 +21,6 @@ package gin
// //
// If the result of this process is an empty string, "/" is returned. // If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string { func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/" // Turn empty string into "/"
if p == "" { if p == "" {
return "/" return "/"
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
} }
b[w] = c b[w] = c
} }
// removeRepeatedChar removes multiple consecutive 'char's from a string.
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
func removeRepeatedChar(s string, char byte) string {
// Check if there are any consecutive chars
hasRepeatedChar := false
for i := 1; i < len(s); i++ {
if s[i] == char && s[i-1] == char {
hasRepeatedChar = true
break
}
}
if !hasRepeatedChar {
return s
}
// Reasonably sized buffer on stack to avoid allocations in the common case.
buf := make([]byte, 0, stackBufSize)
// Invariants:
// reading from s; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
r := 0
w := 0
for n := len(s); r < n; {
if s[r] == char {
// Write the first char
bufApp(&buf, s, w, char)
w++
r++
// Skip all consecutive chars
for r < n && s[r] == char {
r++
}
} else {
// Copy non-char character
bufApp(&buf, s, w, s[r])
w++
r++
}
}
// If the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise, return a new string from the buffer.
if len(buf) == 0 {
return s[:w]
}
return string(buf[:w])
}

View File

@ -94,7 +94,7 @@ func TestPathCleanMallocs(t *testing.T) {
func BenchmarkPathClean(b *testing.B) { func BenchmarkPathClean(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
for _, test := range cleanTests { for _, test := range cleanTests {
cleanPath(test.path) cleanPath(test.path)
} }
@ -134,12 +134,59 @@ func TestPathCleanLong(t *testing.T) {
func BenchmarkPathCleanLong(b *testing.B) { func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths() cleanTests := genLongPaths()
b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
for _, test := range cleanTests { for _, test := range cleanTests {
cleanPath(test.path) cleanPath(test.path)
} }
} }
} }
func TestRemoveRepeatedChar(t *testing.T) {
testCases := []struct {
name string
str string
char byte
want string
}{
{
name: "empty",
str: "",
char: 'a',
want: "",
},
{
name: "noSlash",
str: "abc",
char: ',',
want: "abc",
},
{
name: "withSlash",
str: "/a/b/c/",
char: '/',
want: "/a/b/c/",
},
{
name: "withRepeatedSlashes",
str: "/a//b///c////",
char: '/',
want: "/a/b/c/",
},
{
name: "threeSlashes",
str: "///",
char: '/',
want: "/",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := removeRepeatedChar(tc.str, tc.char)
assert.Equal(t, tc.want, res)
})
}
}

View File

@ -68,6 +68,9 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
} }
} }
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
brokenPipe = true
}
if logger != nil { if logger != nil {
const stackSkip = 3 const stackSkip = 3
if brokenPipe { if brokenPipe {

View File

@ -142,6 +142,30 @@ func TestPanicWithBrokenPipe(t *testing.T) {
} }
} }
// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe
func TestPanicWithAbortHandler(t *testing.T) {
const expectCode = 204
var buf strings.Builder
router := New()
router.Use(RecoveryWithWriter(&buf))
router.GET("/recovery", func(c *Context) {
// Start writing response
c.Header("X-Test", "Value")
c.Status(expectCode)
// Panic with ErrAbortHandler which should be treated as broken pipe
panic(http.ErrAbortHandler)
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
out := buf.String()
assert.Contains(t, out, "net/http: abort Handler")
assert.NotContains(t, out, "panic recovered")
}
func TestCustomRecoveryWithWriter(t *testing.T) { func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder) errBuffer := new(strings.Builder)
buffer := new(strings.Builder) buffer := new(strings.Builder)

View File

@ -27,7 +27,7 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
} }
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
} }
r.writeHeaders(w, r.Headers) r.writeHeaders(w)
_, err = io.Copy(w, r.Reader) _, err = io.Copy(w, r.Reader)
return return
} }
@ -37,10 +37,10 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType}) writeContentType(w, []string{r.ContentType})
} }
// writeHeaders writes custom Header. // writeHeaders writes headers from r.Headers into response.
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) { func (r Reader) writeHeaders(w http.ResponseWriter) {
header := w.Header() header := w.Header()
for k, v := range headers { for k, v := range r.Headers {
if header.Get(k) == "" { if header.Get(k) == "" {
header.Set(k, v) header.Set(k, v)
} }

View File

@ -17,7 +17,7 @@ const (
defaultStatus = http.StatusOK defaultStatus = http.StatusOK
) )
var errHijackAlreadyWritten = errors.New("gin: response already written") var errHijackAlreadyWritten = errors.New("gin: response body already written")
// ResponseWriter ... // ResponseWriter ...
type ResponseWriter interface { type ResponseWriter interface {
@ -109,7 +109,9 @@ func (w *responseWriter) Written() bool {
// Hijack implements the http.Hijacker interface. // Hijack implements the http.Hijacker interface.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if w.Written() { // Allow hijacking before any data is written (size == -1) or after headers are written (size == 0),
// but not after body data is written (size > 0). For compatibility with websocket libraries (e.g., github.com/coder/websocket)
if w.size > 0 {
return nil, nil, errHijackAlreadyWritten return nil, nil, errHijackAlreadyWritten
} }
if w.size < 0 { if w.size < 0 {

View File

@ -194,6 +194,64 @@ func TestResponseWriterHijackAfterWrite(t *testing.T) {
} }
} }
// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
action: func(w ResponseWriter) error {
w.WriteHeaderNow() // Simulate websocket.Accept() behavior
return nil
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: true, // NEW BEHAVIOR: allow hijack after just header write
expectWrittenAfterHijack: true,
expectError: nil,
},
{
name: "hijack after WriteHeaderNow + Write should fail",
action: func(w ResponseWriter) error {
w.WriteHeaderNow()
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
_, _, hijackErr := w.Hijack()
if tc.expectError == nil {
require.NoError(t, hijackErr, "expected hijack to succeed")
} else {
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
}
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
func TestResponseWriterFlush(t *testing.T) { func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{} writer := &responseWriter{}

View File

@ -4,7 +4,11 @@
package gin package gin
import "net/http" import (
"fmt"
"net/http"
"time"
)
// CreateTestContext returns a fresh Engine and a Context associated with it. // CreateTestContext returns a fresh Engine and a Context associated with it.
// This is useful for tests that need to set up a new Gin engine instance // This is useful for tests that need to set up a new Gin engine instance
@ -29,3 +33,28 @@ func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
c.writermem.reset(w) c.writermem.reset(w)
return return
} }
// waitForServerReady waits for a server to be ready by making HTTP requests
// with exponential backoff. This is more reliable than time.Sleep() for testing.
func waitForServerReady(url string, maxAttempts int) error {
client := &http.Client{
Timeout: 100 * time.Millisecond,
}
for i := 0; i < maxAttempts; i++ {
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
return nil
}
// Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms...
backoff := time.Duration(10*(1<<uint(i))) * time.Millisecond
if backoff > 500*time.Millisecond {
backoff = 500 * time.Millisecond
}
time.Sleep(backoff)
}
return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts)
}

29
tree.go
View File

@ -5,7 +5,7 @@
package gin package gin
import ( import (
"bytes" "math"
"net/url" "net/url"
"strings" "strings"
"unicode" "unicode"
@ -14,12 +14,6 @@ import (
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
) )
var (
strColon = []byte(":")
strStar = []byte("*")
strSlash = []byte("/")
)
// Param is a single URL parameter, consisting of a key and a value. // Param is a single URL parameter, consisting of a key and a value.
type Param struct { type Param struct {
Key string Key string
@ -84,17 +78,22 @@ func (n *node) addChild(child *node) {
} }
} }
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
func safeUint16(n int) uint16 {
if n > math.MaxUint16 {
return math.MaxUint16
}
return uint16(n)
}
func countParams(path string) uint16 { func countParams(path string) uint16 {
var n uint16 colons := strings.Count(path, ":")
s := bytesconv.StringToBytes(path) stars := strings.Count(path, "*")
n += uint16(bytes.Count(s, strColon)) return safeUint16(colons + stars)
n += uint16(bytes.Count(s, strStar))
return n
} }
func countSections(path string) uint16 { func countSections(path string) uint16 {
s := bytesconv.StringToBytes(path) return safeUint16(strings.Count(path, "/"))
return uint16(bytes.Count(s, strSlash))
} }
type nodeType uint8 type nodeType uint8
@ -383,7 +382,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
} }
n.addChild(child) n.addChild(child)
n.indices = string('/') n.indices = "/"
n = child n = child
n.priority++ n.priority++

View File

@ -19,7 +19,7 @@ func init() {
} }
func BenchmarkParseAccept(b *testing.B) { func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
} }
} }

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.11.0"