feat: release v0.27 unified HTTP limits and safe request shutdown

This commit is contained in:
2026-09-29 23:24:19 +08:00
parent 859f1851b1
commit 3b00bb55d1
42 changed files with 1191 additions and 70 deletions

View File

@@ -238,7 +238,8 @@ func WithLogConfig(settings logx.Config) Option {
}
}
// WithShutdownTimeout controls how long RunContext waits for active requests.
// WithShutdownTimeout controls the graceful HTTP drain budget. After timeout,
// HTTP.ShutdownCancelTimeout separately bounds the wait for canceled handlers.
func WithShutdownTimeout(timeout time.Duration) Option {
return func(cfg *Config) {
if timeout > 0 {

View File

@@ -3,17 +3,28 @@ package config
import (
"log"
"time"
"git.yuntuo.hk/k2safe/go-omni/internal/httplimits"
)
// HTTPConfig controls built-in HTTP servers. Positive values override the
// selected transport's defaults; zero keeps its existing behavior. Timeouts
// follow that transport's semantics, not a handler execution deadline.
// HTTPConfig controls HTTP serving. Network timeouts follow the selected
// transport's semantics, not a handler execution deadline. Body limits and
// shutdown cancellation grace also apply to App used with an external server.
type HTTPConfig struct {
ReadHeaderTimeout time.Duration `json:"readHeaderTimeout"`
ReadTimeout time.Duration `json:"readTimeout"`
WriteTimeout time.Duration `json:"writeTimeout"`
IdleTimeout time.Duration `json:"idleTimeout"`
MaxHeaderBytes int `json:"maxHeaderBytes"`
// DefaultMaxBodyBytes applies to App routes without an explicit limit.
// Zero uses 8 MiB, or the hard limit if smaller. Route limits may override it.
DefaultMaxBodyBytes int64 `json:"defaultMaxBodyBytes"`
// MaxBodyBytes is an optional service hard limit. Zero adds no hard limit
// below the framework's supported maximum of 1 TiB.
MaxBodyBytes int64 `json:"maxBodyBytes"`
// ShutdownCancelTimeout bounds the wait for handlers after cancellation.
// Zero uses 5s. Resources are retained if handlers have not returned.
ShutdownCancelTimeout time.Duration `json:"shutdownCancelTimeout"`
// ErrorLog is a Go-only hook for the nethttp server's internal diagnostics.
ErrorLog *log.Logger `json:"-"`
}
@@ -27,6 +38,7 @@ func (c HTTPConfig) Validate() error {
{"readTimeout", c.ReadTimeout},
{"writeTimeout", c.WriteTimeout},
{"idleTimeout", c.IdleTimeout},
{"shutdownCancelTimeout", c.ShutdownCancelTimeout},
} {
if field.value < 0 {
return &ConfigError{Field: "http." + field.name, Message: "must be nonnegative"}
@@ -35,11 +47,22 @@ func (c HTTPConfig) Validate() error {
if c.MaxHeaderBytes < 0 {
return &ConfigError{Field: "http.maxHeaderBytes", Message: "must be nonnegative"}
}
for _, field := range []struct {
name string
value int64
}{{"defaultMaxBodyBytes", c.DefaultMaxBodyBytes}, {"maxBodyBytes", c.MaxBodyBytes}} {
if field.value < 0 || field.value > httplimits.MaxBodyBytes {
return &ConfigError{Field: "http." + field.name, Message: "must be 0..1 TiB (bytes)"}
}
}
if c.MaxBodyBytes > 0 && c.DefaultMaxBodyBytes > c.MaxBodyBytes {
return &ConfigError{Field: "http.defaultMaxBodyBytes", Message: "must not exceed http.maxBodyBytes"}
}
return nil
}
// WithHTTPConfig sets the built-in server's HTTP options. RunWith uses a
// caller-owned Runner and does not apply these options to that runner.
// WithHTTPConfig sets HTTP options. RunWith leaves network configuration to the
// caller-owned Runner, but App body limits and cancellation grace still apply.
func WithHTTPConfig(settings HTTPConfig) Option {
return func(cfg *Config) { cfg.HTTP = settings }
}

View File

@@ -93,3 +93,40 @@ func TestHTTPInvalidAndUnsupportedSettings(t *testing.T) {
}
}
}
func TestHTTPBodyAndCancellationSettings(t *testing.T) {
for _, cfg := range []config.HTTPConfig{
{DefaultMaxBodyBytes: -1}, {MaxBodyBytes: -1}, {DefaultMaxBodyBytes: 1<<40 + 1}, {MaxBodyBytes: 1<<40 + 1},
{DefaultMaxBodyBytes: 1024, MaxBodyBytes: 512}, {ShutdownCancelTimeout: -time.Second},
} {
if err := cfg.Validate(); err == nil {
t.Fatalf("invalid HTTP config accepted: %+v", cfg)
}
}
for _, transport := range []string{config.TransportNetHTTP, config.TransportNative, config.TransportHTTP2} {
cfg := config.Default()
cfg.Transport = transport
cfg.TLS = config.TLSConfig{CertFile: "c", KeyFile: "k"}
cfg.HTTP = config.HTTPConfig{DefaultMaxBodyBytes: 1 << 20, MaxBodyBytes: 16 << 20, ShutdownCancelTimeout: time.Second}
if err := cfg.ValidateTransport(); err != nil {
t.Fatal(err)
}
}
t.Setenv("OMNI_BODY_HARD", "16777216")
for _, file := range []struct{ name, body string }{
{"config.json", `{"http":{"defaultMaxBodyBytes":1048576,"maxBodyBytes":"${OMNI_BODY_HARD}","shutdownCancelTimeout":"250ms"}}`},
{"config.yaml", "http:\n defaultMaxBodyBytes: 1048576\n maxBodyBytes: ${OMNI_BODY_HARD}\n shutdownCancelTimeout: 250ms\n"},
} {
path := filepath.Join(t.TempDir(), file.name)
if err := os.WriteFile(path, []byte(file.body), 0600); err != nil {
t.Fatal(err)
}
cfg, err := config.LoadFile(path)
if err != nil {
t.Fatal(err)
}
if cfg.HTTP.DefaultMaxBodyBytes != 1<<20 || cfg.HTTP.MaxBodyBytes != 16<<20 || cfg.HTTP.ShutdownCancelTimeout != 250*time.Millisecond {
t.Fatalf("loaded %+v", cfg.HTTP)
}
}
}