mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-04 17:58:14 +08:00
claude code不添加skill和插件生成的代码
This commit is contained in:
parent
d3ffc99852
commit
47de2f52db
45
config.go
Normal file
45
config.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
72
config_test.go
Normal file
72
config_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
32
gin.go
32
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 enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
|
||||||
ContextWithFallback bool
|
ContextWithFallback bool
|
||||||
|
|
||||||
|
// MaxConns limits the maximum number of concurrent connections.
|
||||||
|
// 0 means no limit (default behavior).
|
||||||
|
MaxConns int64
|
||||||
|
|
||||||
delims render.Delims
|
delims render.Delims
|
||||||
secureJSONPrefix string
|
secureJSONPrefix string
|
||||||
HTMLRender render.HTMLRender
|
HTMLRender render.HTMLRender
|
||||||
@ -534,6 +538,17 @@ func parseIP(ip string) net.IP {
|
|||||||
return parsedIP
|
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.
|
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
@ -547,11 +562,15 @@ 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)
|
||||||
|
ln, err := net.Listen("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
server := &http.Server{ // #nosec G112
|
server := &http.Server{ // #nosec G112
|
||||||
Addr: address,
|
Addr: address,
|
||||||
Handler: engine.Handler(),
|
Handler: engine.Handler(),
|
||||||
}
|
}
|
||||||
err = server.ListenAndServe()
|
err = server.Serve(engine.wrapListener(ln))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,7 +590,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
|||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: engine.Handler(),
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,7 +620,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
|||||||
server := &http.Server{ // #nosec G112
|
server := &http.Server{ // #nosec G112
|
||||||
Handler: engine.Handler(),
|
Handler: engine.Handler(),
|
||||||
}
|
}
|
||||||
err = server.Serve(listener)
|
err = server.Serve(engine.wrapListener(listener))
|
||||||
return
|
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.
|
// 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)
|
// 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: 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) {
|
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
|
||||||
debugPrint("Listening and serving QUIC on %s\n", addr)
|
debugPrint("Listening and serving QUIC on %s\n", addr)
|
||||||
defer func() { debugPrintError(err) }()
|
defer func() { debugPrintError(err) }()
|
||||||
@ -654,7 +678,7 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
|||||||
server := &http.Server{ // #nosec G112
|
server := &http.Server{ // #nosec G112
|
||||||
Handler: engine.Handler(),
|
Handler: engine.Handler(),
|
||||||
}
|
}
|
||||||
err = server.Serve(listener)
|
err = server.Serve(engine.wrapListener(listener))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
listener.go
Normal file
47
listener.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
163
listener_test.go
Normal file
163
listener_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
637
my_changes.patch
Normal file
637
my_changes.patch
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
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
|
||||||
1
testdata/config.yaml
vendored
Normal file
1
testdata/config.yaml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
max_conns: 100
|
||||||
Loading…
x
Reference in New Issue
Block a user