feat: release v0.27 unified HTTP limits and safe request shutdown
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user