mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-14 15:11:18 +08:00
Compare commits
6 Commits
7a5ab077a4
...
f01262148b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01262148b | ||
|
|
c3d5a28ed6 | ||
|
|
acc55e049e | ||
|
|
0c0e99d253 | ||
|
|
dceb61e6e7 | ||
|
|
c9ab7eb86f |
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
ignore-unfixed: true
|
||||
|
||||
- name: Upload Trivy results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
11
README.md
11
README.md
@ -3,6 +3,7 @@
|
||||
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
|
||||
|
||||
[](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
|
||||
[](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
|
||||
[](https://codecov.io/gh/gin-gonic/gin)
|
||||
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
||||
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||
@ -62,6 +63,7 @@ Here's a complete example that demonstrates Gin's simplicity:
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -70,7 +72,7 @@ import (
|
||||
func main() {
|
||||
// Create a Gin router with default middleware (logger and recovery)
|
||||
r := gin.Default()
|
||||
|
||||
|
||||
// Define a simple GET endpoint
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
// Return JSON response
|
||||
@ -78,10 +80,12 @@ func main() {
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// Start server on port 8080 (default)
|
||||
// Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows)
|
||||
r.Run()
|
||||
if err := r.Run(); err != nil {
|
||||
log.Fatalf("failed to run server: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -190,7 +194,6 @@ Gin has a rich ecosystem of middleware for common web development needs. Explore
|
||||
- CORS, Rate limiting, Compression
|
||||
- Logging, Metrics, Tracing
|
||||
- Static file serving, Template engines
|
||||
|
||||
- **[gin-gonic/contrib](https://github.com/gin-gonic/contrib)** - Additional community middleware
|
||||
|
||||
## 🏢 Production Usage
|
||||
|
||||
22
context.go
22
context.go
@ -39,6 +39,7 @@ const (
|
||||
MIMEYAML = binding.MIMEYAML
|
||||
MIMEYAML2 = binding.MIMEYAML2
|
||||
MIMETOML = binding.MIMETOML
|
||||
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||
)
|
||||
|
||||
// BodyBytesKey indicates a default body bytes key.
|
||||
@ -1280,14 +1281,15 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
|
||||
|
||||
// Negotiate contains all negotiations data.
|
||||
type Negotiate struct {
|
||||
Offered []string
|
||||
HTMLName string
|
||||
HTMLData any
|
||||
JSONData any
|
||||
XMLData any
|
||||
YAMLData any
|
||||
Data any
|
||||
TOMLData any
|
||||
Offered []string
|
||||
HTMLName string
|
||||
HTMLData any
|
||||
JSONData any
|
||||
XMLData any
|
||||
YAMLData any
|
||||
Data any
|
||||
TOMLData any
|
||||
PROTOBUFData any
|
||||
}
|
||||
|
||||
// Negotiate calls different Render according to acceptable Accept format.
|
||||
@ -1313,6 +1315,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
data := chooseData(config.TOMLData, config.Data)
|
||||
c.TOML(code, data)
|
||||
|
||||
case binding.MIMEPROTOBUF:
|
||||
data := chooseData(config.PROTOBUFData, config.Data)
|
||||
c.ProtoBuf(code, data)
|
||||
|
||||
default:
|
||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||
}
|
||||
|
||||
@ -1628,6 +1628,32 @@ func TestContextNegotiationWithHTML(t *testing.T) {
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
81
gin.go
81
gin.go
@ -112,6 +112,15 @@ type Engine struct {
|
||||
// RedirectTrailingSlash is independent of this option.
|
||||
RedirectFixedPath bool
|
||||
|
||||
// TrailingSlashInsensitivity makes the router insensitive to trailing
|
||||
// slashes. It works like RedirectTrailingSlash, but instead of generating a
|
||||
// redirection response to the path with or without the trailing slash, it
|
||||
// will just go to the corresponding handler.
|
||||
//
|
||||
// Enabling this option will make RedirectTrailingSlash ineffective since
|
||||
// no redirection will be performed.
|
||||
TrailingSlashInsensitivity bool
|
||||
|
||||
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
|
||||
// current route, if the current request can not be routed.
|
||||
// If this is the case, the request is answered with 'Method Not Allowed'
|
||||
@ -184,12 +193,13 @@ var _ IRouter = (*Engine)(nil)
|
||||
|
||||
// New returns a new blank Engine instance without any middleware attached.
|
||||
// By default, the configuration is:
|
||||
// - RedirectTrailingSlash: true
|
||||
// - RedirectFixedPath: false
|
||||
// - HandleMethodNotAllowed: false
|
||||
// - ForwardedByClientIP: true
|
||||
// - UseRawPath: false
|
||||
// - UnescapePathValues: true
|
||||
// - RedirectTrailingSlash: true
|
||||
// - RedirectFixedPath: false
|
||||
// - TrailingSlashInsensitivity: false
|
||||
// - HandleMethodNotAllowed: false
|
||||
// - ForwardedByClientIP: true
|
||||
// - UseRawPath: false
|
||||
// - UnescapePathValues: true
|
||||
func New(opts ...OptionFunc) *Engine {
|
||||
debugPrintWARNINGNew()
|
||||
engine := &Engine{
|
||||
@ -198,22 +208,23 @@ func New(opts ...OptionFunc) *Engine {
|
||||
basePath: "/",
|
||||
root: true,
|
||||
},
|
||||
FuncMap: template.FuncMap{},
|
||||
RedirectTrailingSlash: true,
|
||||
RedirectFixedPath: false,
|
||||
HandleMethodNotAllowed: false,
|
||||
ForwardedByClientIP: true,
|
||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||
TrustedPlatform: defaultPlatform,
|
||||
UseRawPath: false,
|
||||
RemoveExtraSlash: false,
|
||||
UnescapePathValues: true,
|
||||
MaxMultipartMemory: defaultMultipartMemory,
|
||||
trees: make(methodTrees, 0, 9),
|
||||
delims: render.Delims{Left: "{{", Right: "}}"},
|
||||
secureJSONPrefix: "while(1);",
|
||||
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||
trustedCIDRs: defaultTrustedCIDRs,
|
||||
FuncMap: template.FuncMap{},
|
||||
RedirectTrailingSlash: true,
|
||||
RedirectFixedPath: false,
|
||||
TrailingSlashInsensitivity: false,
|
||||
HandleMethodNotAllowed: false,
|
||||
ForwardedByClientIP: true,
|
||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||
TrustedPlatform: defaultPlatform,
|
||||
UseRawPath: false,
|
||||
RemoveExtraSlash: false,
|
||||
UnescapePathValues: true,
|
||||
MaxMultipartMemory: defaultMultipartMemory,
|
||||
trees: make(methodTrees, 0, 9),
|
||||
delims: render.Delims{Left: "{{", Right: "}}"},
|
||||
secureJSONPrefix: "while(1);",
|
||||
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||
trustedCIDRs: defaultTrustedCIDRs,
|
||||
}
|
||||
engine.engine = engine
|
||||
engine.pool.New = func() any {
|
||||
@ -593,6 +604,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
}
|
||||
|
||||
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
||||
defer f.Close()
|
||||
listener, err := net.FileListener(f)
|
||||
if err != nil {
|
||||
return
|
||||
@ -691,6 +703,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
return
|
||||
}
|
||||
if httpMethod != http.MethodConnect && rPath != "/" {
|
||||
// TrailingSlashInsensitivity has precedence over RedirectTrailingSlash.
|
||||
if value.tsr && engine.TrailingSlashInsensitivity {
|
||||
// Retry with the path with or without the trailing slash.
|
||||
// It should succeed because tsr is true.
|
||||
value = root.getValue(addOrRemoveTrailingSlash(rPath), c.params, c.skippedNodes, unescape)
|
||||
if value.handlers != nil {
|
||||
c.handlers = value.handlers
|
||||
c.fullPath = value.fullPath
|
||||
c.Next()
|
||||
c.writermem.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
}
|
||||
if value.tsr && engine.RedirectTrailingSlash {
|
||||
redirectTrailingSlash(c)
|
||||
return
|
||||
@ -745,6 +770,13 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
||||
c.writermem.WriteHeaderNow()
|
||||
}
|
||||
|
||||
func addOrRemoveTrailingSlash(p string) string {
|
||||
if length := len(p); length > 1 && p[length-1] == '/' {
|
||||
return p[:length-1]
|
||||
}
|
||||
return p + "/"
|
||||
}
|
||||
|
||||
func redirectTrailingSlash(c *Context) {
|
||||
req := c.Request
|
||||
p := req.URL.Path
|
||||
@ -754,10 +786,7 @@ func redirectTrailingSlash(c *Context) {
|
||||
|
||||
p = prefix + "/" + req.URL.Path
|
||||
}
|
||||
req.URL.Path = p + "/"
|
||||
if length := len(p); length > 1 && p[length-1] == '/' {
|
||||
req.URL.Path = p[:length-1]
|
||||
}
|
||||
req.URL.Path = addOrRemoveTrailingSlash(p)
|
||||
redirectRequest(c)
|
||||
}
|
||||
|
||||
|
||||
101
routes_test.go
101
routes_test.go
@ -246,6 +246,107 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestRouteTrailingSlashInsensitivity(t *testing.T) {
|
||||
router := New()
|
||||
router.RedirectTrailingSlash = false
|
||||
router.TrailingSlashInsensitivity = true
|
||||
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
|
||||
router.GET("/path2/", func(c *Context) { c.String(http.StatusOK, "path2") })
|
||||
|
||||
// Test that trailing slash insensitivity works.
|
||||
w := PerformRequest(router, http.MethodGet, "/path/")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "path", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "path", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path2/")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "path2", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path2")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "path2", w.Body.String())
|
||||
|
||||
// If handlers for `/path` and `/path/` are different, the request should not be redirected.
|
||||
router.GET("/path3", func(c *Context) { c.String(http.StatusOK, "path3") })
|
||||
router.GET("/path3/", func(c *Context) { c.String(http.StatusOK, "path3/") })
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path3")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "path3", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path3/")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "path3/", w.Body.String())
|
||||
|
||||
// Should no longer match.
|
||||
router.TrailingSlashInsensitivity = false
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path2")
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
w = PerformRequest(router, http.MethodGet, "/path/")
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func BenchmarkRouteTrailingSlashInsensitivity(b *testing.B) {
|
||||
b.Run("Insensitive", func(b *testing.B) {
|
||||
router := New()
|
||||
router.RedirectTrailingSlash = false
|
||||
router.TrailingSlashInsensitivity = true
|
||||
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Cause an insensitive match. Test if the retry logic is causing
|
||||
// slowdowns.
|
||||
w := PerformRequest(router, http.MethodGet, "/path/")
|
||||
if w.Code != http.StatusOK || w.Body.String() != "path" {
|
||||
b.Fatalf("Expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Exact", func(b *testing.B) {
|
||||
router := New()
|
||||
router.RedirectTrailingSlash = false
|
||||
router.TrailingSlashInsensitivity = false
|
||||
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := PerformRequest(router, http.MethodGet, "/path") // Exact match.
|
||||
if w.Code != http.StatusOK || w.Body.String() != "path" {
|
||||
b.Fatalf("Expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Redirect", func(b *testing.B) {
|
||||
router := New()
|
||||
router.RedirectTrailingSlash = true
|
||||
router.TrailingSlashInsensitivity = false
|
||||
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := PerformRequest(router, http.MethodGet, "/path/") // Redirect.
|
||||
if w.Code != http.StatusMovedPermanently {
|
||||
b.Fatalf("Expected status %d, got %d", http.StatusMovedPermanently, w.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteRedirectFixedPath(t *testing.T) {
|
||||
router := New()
|
||||
router.RedirectFixedPath = true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user