fix(engine): add read header timeout by default

This commit is contained in:
chihskin-afk 2026-08-20 09:52:38 +03:00
parent 34dac209ff
commit 1954c48625
3 changed files with 80 additions and 0 deletions

View File

@ -2091,6 +2091,18 @@ See a complete example in the `https://github.com/gin-gonic/examples/tree/master
> Configure HTTP servers, TLS, proxies, and runtime settings.
### Default Read header timeout
You can change the default read header timeout by `engine.ReadHeaderTimeout` if its necessary.
```go
func main() {
router := gin.Default()
router.ReadHeaderTimeout = 5 * time.Second
http.ListenAndServe(":8000", router)
}
```
### Custom HTTP configuration
Use `http.ListenAndServe()` directly, like this:

15
gin.go
View File

@ -13,6 +13,7 @@ import (
"path"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin/internal/bytesconv"
filesystem "github.com/gin-gonic/gin/internal/fs"
@ -24,6 +25,7 @@ import (
const (
defaultMultipartMemory = 32 << 20 // 32 MB
defaultReadHeaderTimeout = 10 * time.Second
escapedColon = "\\:"
colon = ":"
backslash = "\\"
@ -114,6 +116,17 @@ type Engine struct {
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// ReadHeaderTimeout is the maximum duration for reading the entire
// request header, including the body. A zero or negative value means
// there will be no timeout.
//
// This setting helps protect against Slowloris attacks by limiting
// the time a client can take to send headers. If the timeout expires
// before the full header is received, the server closes the connection.
// It corresponds directly to http.Server.ReadHeaderTimeout in the
// standard library.
ReadHeaderTimeout time.Duration
// 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'
@ -210,6 +223,7 @@ func New(opts ...OptionFunc) *Engine {
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
ReadHeaderTimeout: defaultReadHeaderTimeout,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
@ -550,6 +564,7 @@ func (engine *Engine) Run(addr ...string) (err error) {
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
ReadHeaderTimeout: engine.ReadHeaderTimeout,
}
err = server.ListenAndServe()
return

View File

@ -605,3 +605,56 @@ func TestEscapedColon(t *testing.T) {
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
}
// Tests the ReadHeaderTimeout fail
func TestEngineReadHeaderTimeout(t *testing.T) {
const timeout = 200 * time.Millisecond
router := New()
router.ReadHeaderTimeout = timeout
router.GET("/test", func(c *Context) {
c.String(http.StatusOK, "ok")
})
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
go func() {
_ = http.Serve(ln, router.Handler())
}()
conn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer conn.Close()
_, err = conn.Write([]byte("GET /test HTTP/1.1\r\nHost: localhost\r\n"))
require.NoError(t, err)
conn.SetReadDeadline(time.Now().Add(timeout + 500*time.Millisecond))
buf := make([]byte, 1024)
_, err = conn.Read(buf)
assert.Error(t, err, "expected connection to be closed by server due to ReadHeaderTimeout")
if err == nil {
t.Fatalf("expected error but got response: %s", string(buf))
}
}
// Tests the ReadHeaderTimeout success case
func TestEngineReadHeaderTimeoutSuccess(t *testing.T) {
router := New()
router.ReadHeaderTimeout = 5 * time.Second
router.GET("/test", func(c *Context) {
c.String(http.StatusOK, "success")
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/test", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "success", w.Body.String())
}