gin/my_changes.patch
2026-04-21 19:45:17 +08:00

638 lines
18 KiB
Diff

diff --git a/claude.change.patch b/claude.change.patch
new file mode 100644
index 0000000..be3867b
--- /dev/null
+++ b/claude.change.patch
@@ -0,0 +1,89 @@
+diff --git a/gin.go b/gin.go
+index 2e033bf..2ccc093 100644
+--- a/gin.go
++++ b/gin.go
+@@ -172,6 +172,10 @@ type Engine struct {
+ // ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
+ ContextWithFallback bool
+
++ // MaxConns limits the maximum number of concurrent connections.
++ // 0 means no limit (default behavior).
++ MaxConns int64
++
+ delims render.Delims
+ secureJSONPrefix string
+ HTMLRender render.HTMLRender
+@@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
+ return parsedIP
+ }
+
++// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
++func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
++ if engine.MaxConns <= 0 {
++ return ln
++ }
++ return &limitedListener{
++ Listener: ln,
++ sem: make(chan struct{}, engine.MaxConns),
++ }
++}
++
+ // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
+ // It is a shortcut for http.ListenAndServe(addr, router)
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
+@@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
+ engine.updateRouteTrees()
+ address := resolveAddress(addr)
+ debugPrint("Listening and serving HTTP on %s\n", address)
++ ln, err := net.Listen("tcp", address)
++ if err != nil {
++ return
++ }
+ server := &http.Server{ // #nosec G112
+ Addr: address,
+ Handler: engine.Handler(),
+ }
+- err = server.ListenAndServe()
++ err = server.Serve(engine.wrapListener(ln))
+ return
+ }
+
+@@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
+ Addr: addr,
+ Handler: engine.Handler(),
+ }
+- err = server.ListenAndServeTLS(certFile, keyFile)
++ ln, err := net.Listen("tcp", addr)
++ if err != nil {
++ return
++ }
++ err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
+ return
+ }
+
+@@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
+ server := &http.Server{ // #nosec G112
+ Handler: engine.Handler(),
+ }
+- err = server.Serve(listener)
++ err = server.Serve(engine.wrapListener(listener))
+ return
+ }
+
+@@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
+ // RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
+ // It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
++// Note: MaxConns is not supported for QUIC due to protocol limitations.
+ func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
+ debugPrint("Listening and serving QUIC on %s\n", addr)
+ defer func() { debugPrintError(err) }()
+@@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
+ server := &http.Server{ // #nosec G112
+ Handler: engine.Handler(),
+ }
+- err = server.Serve(listener)
++ err = server.Serve(engine.wrapListener(listener))
+ return
+ }
+
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..232163c
--- /dev/null
+++ b/config.go
@@ -0,0 +1,45 @@
+// 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 gin
+
+import (
+ "os"
+
+ "github.com/goccy/go-yaml"
+)
+
+// Config represents the YAML configuration structure for Gin.
+type Config struct {
+ // MaxConns limits the maximum number of concurrent connections.
+ // 0 means no limit (default behavior).
+ MaxConns int64 `yaml:"max_conns"`
+}
+
+// LoadConfig reads configuration from a YAML file and returns an OptionFunc
+// that applies the configuration to an Engine.
+func LoadConfig(path string) (OptionFunc, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var cfg Config
+ if err := yaml.Unmarshal(data, &cfg); err != nil {
+ return nil, err
+ }
+
+ return func(e *Engine) {
+ if cfg.MaxConns > 0 {
+ e.MaxConns = cfg.MaxConns
+ }
+ }, nil
+}
+
+// WithMaxConns creates an OptionFunc that sets the maximum concurrent connections.
+func WithMaxConns(n int64) OptionFunc {
+ return func(e *Engine) {
+ e.MaxConns = n
+ }
+}
diff --git a/config_test.go b/config_test.go
new file mode 100644
index 0000000..6a65fe5
--- /dev/null
+++ b/config_test.go
@@ -0,0 +1,72 @@
+// 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 gin
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLoadConfig(t *testing.T) {
+ path := filepath.Join("testdata", "config.yaml")
+ opt, err := LoadConfig(path)
+ require.NoError(t, err)
+ require.NotNil(t, opt)
+
+ engine := New()
+ opt(engine)
+ assert.Equal(t, int64(100), engine.MaxConns)
+}
+
+func TestLoadConfigNotFound(t *testing.T) {
+ opt, err := LoadConfig("nonexistent.yaml")
+ assert.Error(t, err)
+ assert.Nil(t, opt)
+}
+
+func TestLoadConfigInvalidYAML(t *testing.T) {
+ // Create a temporary file with invalid YAML
+ tmpFile, err := os.CreateTemp("", "invalid_*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpFile.Name())
+
+ _, err = tmpFile.WriteString("max_conns: [invalid")
+ require.NoError(t, err)
+ tmpFile.Close()
+
+ opt, err := LoadConfig(tmpFile.Name())
+ assert.Error(t, err)
+ assert.Nil(t, opt)
+}
+
+func TestLoadConfigEmpty(t *testing.T) {
+ // Create a temporary empty config file
+ tmpFile, err := os.CreateTemp("", "empty_*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpFile.Name())
+ tmpFile.Close()
+
+ opt, err := LoadConfig(tmpFile.Name())
+ require.NoError(t, err)
+ require.NotNil(t, opt)
+
+ engine := New()
+ opt(engine)
+ assert.Equal(t, int64(0), engine.MaxConns)
+}
+
+func TestWithMaxConns(t *testing.T) {
+ engine := New(WithMaxConns(50))
+ assert.Equal(t, int64(50), engine.MaxConns)
+}
+
+func TestWithMaxConnsZero(t *testing.T) {
+ engine := New(WithMaxConns(0))
+ assert.Equal(t, int64(0), engine.MaxConns)
+}
diff --git a/gin.go b/gin.go
index 2e033bf..2ccc093 100644
--- a/gin.go
+++ b/gin.go
@@ -172,6 +172,10 @@ type Engine struct {
// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
ContextWithFallback bool
+ // MaxConns limits the maximum number of concurrent connections.
+ // 0 means no limit (default behavior).
+ MaxConns int64
+
delims render.Delims
secureJSONPrefix string
HTMLRender render.HTMLRender
@@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
return parsedIP
}
+// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
+func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
+ if engine.MaxConns <= 0 {
+ return ln
+ }
+ return &limitedListener{
+ Listener: ln,
+ sem: make(chan struct{}, engine.MaxConns),
+ }
+}
+
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
@@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
+ ln, err := net.Listen("tcp", address)
+ if err != nil {
+ return
+ }
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
- err = server.ListenAndServe()
+ err = server.Serve(engine.wrapListener(ln))
return
}
@@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
Addr: addr,
Handler: engine.Handler(),
}
- err = server.ListenAndServeTLS(certFile, keyFile)
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return
+ }
+ err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
return
}
@@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
- err = server.Serve(listener)
+ err = server.Serve(engine.wrapListener(listener))
return
}
@@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
+// Note: MaxConns is not supported for QUIC due to protocol limitations.
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving QUIC on %s\n", addr)
defer func() { debugPrintError(err) }()
@@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
- err = server.Serve(listener)
+ err = server.Serve(engine.wrapListener(listener))
return
}
diff --git a/listener.go b/listener.go
new file mode 100644
index 0000000..b31679a
--- /dev/null
+++ b/listener.go
@@ -0,0 +1,47 @@
+// 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 gin
+
+import (
+ "net"
+ "sync"
+)
+
+// limitedListener wraps a net.Listener and limits the number of concurrent connections
+// using a buffered channel as a semaphore.
+type limitedListener struct {
+ net.Listener
+ sem chan struct{}
+}
+
+// Accept accepts a new connection. If the connection limit has been reached,
+// the new connection is immediately closed.
+func (l *limitedListener) Accept() (net.Conn, error) {
+ conn, err := l.Listener.Accept()
+ if err != nil {
+ return nil, err
+ }
+
+ select {
+ case l.sem <- struct{}{}:
+ return &limitedConn{Conn: conn, sem: l.sem}, nil
+ default:
+ conn.Close()
+ return nil, nil
+ }
+}
+
+// limitedConn wraps a net.Conn and releases the semaphore slot on Close.
+type limitedConn struct {
+ net.Conn
+ sem chan struct{}
+ closeOnce sync.Once
+}
+
+// Close closes the connection and releases the semaphore slot.
+func (c *limitedConn) Close() error {
+ c.closeOnce.Do(func() { <-c.sem })
+ return c.Conn.Close()
+}
diff --git a/listener_test.go b/listener_test.go
new file mode 100644
index 0000000..32ce702
--- /dev/null
+++ b/listener_test.go
@@ -0,0 +1,163 @@
+// 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 gin
+
+import (
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLimitedListenerUnderLimit(t *testing.T) {
+ router := New(WithMaxConns(10))
+ router.GET("/", func(c *Context) {
+ c.String(http.StatusOK, "ok")
+ })
+
+ server := httptest.NewServer(router.Handler())
+ defer server.Close()
+
+ // Should be able to make requests under limit
+ resp, err := http.Get(server.URL)
+ require.NoError(t, err)
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ resp.Body.Close()
+}
+
+func TestLimitedListenerAtLimit(t *testing.T) {
+ // Create a server that holds connections
+ var activeConns atomic.Int32
+ var wg sync.WaitGroup
+ block := make(chan struct{})
+
+ router := New(WithMaxConns(2))
+ router.GET("/", func(c *Context) {
+ activeConns.Add(1)
+ wg.Add(1)
+ <-block // Block until test releases
+ activeConns.Add(-1)
+ wg.Done()
+ c.String(http.StatusOK, "ok")
+ })
+
+ server := httptest.NewServer(router.Handler())
+ defer server.Close()
+
+ // Start 2 requests that will block
+ for i := 0; i < 2; i++ {
+ go func() {
+ resp, err := http.Get(server.URL)
+ if err == nil {
+ resp.Body.Close()
+ }
+ }()
+ }
+
+ // Wait for both connections to be active
+ require.Eventually(t, func() bool {
+ return activeConns.Load() == 2
+ }, 2*time.Second, 10*time.Millisecond)
+
+ // Third request should be rejected immediately
+ client := &http.Client{Timeout: 500 * time.Millisecond}
+ _, err := client.Get(server.URL)
+ // Connection should be rejected
+ assert.Error(t, err, "expected connection to be rejected")
+
+ // Release the blocked connections
+ close(block)
+ wg.Wait()
+}
+
+func TestLimitedConnRelease(t *testing.T) {
+ block := make(chan struct{})
+
+ router := New(WithMaxConns(1))
+ router.GET("/", func(c *Context) {
+ <-block
+ c.String(http.StatusOK, "ok")
+ })
+
+ server := httptest.NewServer(router.Handler())
+ defer server.Close()
+
+ // Start one blocking request
+ var firstDone atomic.Bool
+ go func() {
+ resp, err := http.Get(server.URL)
+ if err == nil {
+ resp.Body.Close()
+ firstDone.Store(true)
+ }
+ }()
+
+ // Give the first request time to start
+ time.Sleep(100 * time.Millisecond)
+
+ // Second request should fail
+ client := &http.Client{Timeout: 200 * time.Millisecond}
+ _, err := client.Get(server.URL)
+ assert.Error(t, err, "expected connection to be rejected when limit reached")
+
+ // Release the first connection
+ close(block)
+
+ // Eventually first request should complete
+ require.Eventually(t, firstDone.Load, 2*time.Second, 10*time.Millisecond)
+}
+
+func TestLimitedListenerZeroLimit(t *testing.T) {
+ router := New(WithMaxConns(0))
+ router.GET("/", func(c *Context) {
+ c.String(http.StatusOK, "ok")
+ })
+
+ server := httptest.NewServer(router.Handler())
+ defer server.Close()
+
+ // Should allow unlimited connections (zero means no limit)
+ for i := 0; i < 5; i++ {
+ resp, err := http.Get(server.URL)
+ require.NoError(t, err)
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ resp.Body.Close()
+ }
+}
+
+func TestWrapListenerNoLimit(t *testing.T) {
+ engine := New()
+ assert.Equal(t, int64(0), engine.MaxConns)
+
+ // Create a dummy listener
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ // Should return original listener when no limit
+ wrapped := engine.wrapListener(ln)
+ assert.Equal(t, ln, wrapped)
+}
+
+func TestWrapListenerWithLimit(t *testing.T) {
+ engine := New(WithMaxConns(5))
+
+ // Create a dummy listener
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer ln.Close()
+
+ // Should return wrapped listener
+ wrapped := engine.wrapListener(ln)
+ assert.NotNil(t, wrapped)
+ _, ok := wrapped.(*limitedListener)
+ assert.True(t, ok)
+}
diff --git a/my_changes.patch b/my_changes.patch
new file mode 100644
index 0000000..be3867b
--- /dev/null
+++ b/my_changes.patch
@@ -0,0 +1,89 @@
+diff --git a/gin.go b/gin.go
+index 2e033bf..2ccc093 100644
+--- a/gin.go
++++ b/gin.go
+@@ -172,6 +172,10 @@ type Engine struct {
+ // ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
+ ContextWithFallback bool
+
++ // MaxConns limits the maximum number of concurrent connections.
++ // 0 means no limit (default behavior).
++ MaxConns int64
++
+ delims render.Delims
+ secureJSONPrefix string
+ HTMLRender render.HTMLRender
+@@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
+ return parsedIP
+ }
+
++// wrapListener wraps a net.Listener with connection limiting if MaxConns > 0.
++func (engine *Engine) wrapListener(ln net.Listener) net.Listener {
++ if engine.MaxConns <= 0 {
++ return ln
++ }
++ return &limitedListener{
++ Listener: ln,
++ sem: make(chan struct{}, engine.MaxConns),
++ }
++}
++
+ // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
+ // It is a shortcut for http.ListenAndServe(addr, router)
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
+@@ -547,11 +562,15 @@ func (engine *Engine) Run(addr ...string) (err error) {
+ engine.updateRouteTrees()
+ address := resolveAddress(addr)
+ debugPrint("Listening and serving HTTP on %s\n", address)
++ ln, err := net.Listen("tcp", address)
++ if err != nil {
++ return
++ }
+ server := &http.Server{ // #nosec G112
+ Addr: address,
+ Handler: engine.Handler(),
+ }
+- err = server.ListenAndServe()
++ err = server.Serve(engine.wrapListener(ln))
+ return
+ }
+
+@@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
+ Addr: addr,
+ Handler: engine.Handler(),
+ }
+- err = server.ListenAndServeTLS(certFile, keyFile)
++ ln, err := net.Listen("tcp", addr)
++ if err != nil {
++ return
++ }
++ err = server.ServeTLS(engine.wrapListener(ln), certFile, keyFile)
+ return
+ }
+
+@@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
+ server := &http.Server{ // #nosec G112
+ Handler: engine.Handler(),
+ }
+- err = server.Serve(listener)
++ err = server.Serve(engine.wrapListener(listener))
+ return
+ }
+
+@@ -627,6 +650,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
+ // RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
+ // It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
+ // Note: this method will block the calling goroutine indefinitely unless an error happens.
++// Note: MaxConns is not supported for QUIC due to protocol limitations.
+ func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
+ debugPrint("Listening and serving QUIC on %s\n", addr)
+ defer func() { debugPrintError(err) }()
+@@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
+ server := &http.Server{ // #nosec G112
+ Handler: engine.Handler(),
+ }
+- err = server.Serve(listener)
++ err = server.Serve(engine.wrapListener(listener))
+ return
+ }
+
diff --git a/testdata/config.yaml b/testdata/config.yaml
new file mode 100644
index 0000000..b922394
--- /dev/null
+++ b/testdata/config.yaml
@@ -0,0 +1 @@
+max_conns: 100