feat: expose HTTP2 header and protocol limits

This commit is contained in:
2026-09-22 22:54:55 +08:00
parent 46c5c9406c
commit dd7d3fef0a
4 changed files with 55 additions and 6 deletions

View File

@@ -35,6 +35,7 @@ type Server struct {
MaxConnections int
MaxConcurrentRequests int
MaxConcurrentStreams uint32
MaxHeaderBytes int
MaxBodyBytes int64
IdleTimeout time.Duration
TLSHandshakeTimeout time.Duration
@@ -55,6 +56,7 @@ type Server struct {
accepted atomic.Uint64
rejectedConns atomic.Uint64
rejectedReqs atomic.Uint64
protocolErrors atomic.Uint64
activeConns atomic.Int64
activeRequests atomic.Int64
wg sync.WaitGroup
@@ -66,6 +68,7 @@ type Stats struct {
AcceptedConnections uint64 `json:"acceptedConnections"`
RejectedConnections uint64 `json:"rejectedConnections"`
RejectedRequests uint64 `json:"rejectedRequests"`
ProtocolErrors uint64 `json:"protocolErrors"`
ActiveConnections int64 `json:"activeConnections"`
ActiveRequests int64 `json:"activeRequests"`
}
@@ -89,6 +92,7 @@ func (s *Server) Stats() Stats {
AcceptedConnections: s.accepted.Load(),
RejectedConnections: s.rejectedConns.Load(),
RejectedRequests: s.rejectedReqs.Load(),
ProtocolErrors: s.protocolErrors.Load(),
ActiveConnections: s.activeConns.Load(),
ActiveRequests: s.activeRequests.Load(),
}
@@ -226,8 +230,11 @@ func (s *Server) Serve(ctx context.Context, listener net.Listener) error {
protocolServer := &xhttp2.Server{
MaxConcurrentStreams: s.MaxConcurrentStreams,
IdleTimeout: s.IdleTimeout,
CountError: func(string) {
s.protocolErrors.Add(1)
},
}
shutdownConfig := &http.Server{IdleTimeout: s.IdleTimeout}
shutdownConfig := &http.Server{IdleTimeout: s.IdleTimeout, MaxHeaderBytes: s.MaxHeaderBytes}
if err := xhttp2.ConfigureServer(shutdownConfig, protocolServer); err != nil {
cancel()
s.mu.Lock()

View File

@@ -14,6 +14,7 @@ import (
"math/big"
"net"
"net/http"
"strings"
"sync"
"sync/atomic"
"testing"
@@ -175,6 +176,46 @@ func TestServerRejectsBodyAboveLimitAndRecovers(t *testing.T) {
}
}
func TestServerRejectsHeadersAboveLimitAndRecovers(t *testing.T) {
server := New(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = io.WriteString(writer, "ok")
}))
server.MaxHeaderBytes = 1024
address, stop := startServer(t, server)
defer stop()
transport := &xhttp2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, address)
},
}
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
defer transport.CloseIdleConnections()
request, err := http.NewRequest(http.MethodGet, "http://"+address+"/too-large", nil)
if err != nil {
t.Fatal(err)
}
request.Header.Set("X-Large", strings.Repeat("x", 2048))
response, err := client.Do(request)
if err == nil {
response.Body.Close()
if response.StatusCode < http.StatusBadRequest {
t.Fatalf("oversized headers unexpectedly succeeded with status %d", response.StatusCode)
}
}
response, err = client.Get("http://" + address + "/recovered")
if err != nil {
t.Fatal(err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil || response.StatusCode != http.StatusOK || string(body) != "ok" {
t.Fatalf("header limit did not recover: status=%d body=%q err=%v", response.StatusCode, body, readErr)
}
}
func TestServerMultiplexes128Streams(t *testing.T) {
started := make(chan struct{}, 128)
release := make(chan struct{})
@@ -196,7 +237,8 @@ func TestServerMultiplexes128Streams(t *testing.T) {
address, stop := startServer(t, server)
defer stop()
transport := &xhttp2.Transport{
AllowHTTP: true,
AllowHTTP: true,
StrictMaxConcurrentStreams: true,
DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, address)
},