From ecdc49ad331dac3d13ff4fc73a51050259bb5afb Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Wed, 4 Mar 2026 10:12:57 +0100 Subject: [PATCH] Server: Add HTTP security hardening config options #5471 Signed-off-by: Michael Mayer --- internal/commands/show_config_options_test.go | 3 + internal/commands/show_json_test.go | 76 +++++++++++++++++++ internal/commands/start.go | 3 + internal/config/README.md | 9 ++- internal/config/config_const.go | 9 +++ internal/config/config_server.go | 28 +++++++ internal/config/config_server_test.go | 37 +++++++++ internal/config/flags.go | 18 +++++ internal/config/options.go | 3 + internal/config/report.go | 3 + internal/config/report_test.go | 31 ++++++++ internal/server/README.md | 11 ++- internal/server/start.go | 47 ++++++++---- internal/server/start_test.go | 27 +++++++ 14 files changed, 285 insertions(+), 20 deletions(-) diff --git a/internal/commands/show_config_options_test.go b/internal/commands/show_config_options_test.go index f5467d926..a8bb9e603 100644 --- a/internal/commands/show_config_options_test.go +++ b/internal/commands/show_config_options_test.go @@ -12,4 +12,7 @@ func TestShowConfigOptionsCommand(t *testing.T) { assert.NoError(t, err) assert.Contains(t, output, "PHOTOPRISM_IMPORT_PATH") + assert.Contains(t, output, "PHOTOPRISM_HTTP_HEADER_TIMEOUT") + assert.Contains(t, output, "PHOTOPRISM_HTTP_HEADER_BYTES") + assert.Contains(t, output, "PHOTOPRISM_HTTP_IDLE_TIMEOUT") } diff --git a/internal/commands/show_json_test.go b/internal/commands/show_json_test.go index 15c209548..8947ade01 100644 --- a/internal/commands/show_json_test.go +++ b/internal/commands/show_json_test.go @@ -240,6 +240,82 @@ func TestShowConfigYaml_MarkdownServicesCIDRSectionOrder(t *testing.T) { } } +func TestShowConfigOptions_MarkdownHttpHeaderSettingsOrder(t *testing.T) { + out, err := RunWithTestContext(ShowConfigOptionsCommand, []string{"config-options", "--md"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + webServerHeading := strings.Index(out, "### Web Server") + httpCompression := strings.Index(out, "PHOTOPRISM_HTTP_COMPRESSION") + httpHeaderTimeout := strings.Index(out, "PHOTOPRISM_HTTP_HEADER_TIMEOUT") + httpHeaderBytes := strings.Index(out, "PHOTOPRISM_HTTP_HEADER_BYTES") + httpIdleTimeout := strings.Index(out, "PHOTOPRISM_HTTP_IDLE_TIMEOUT") + httpCachePublic := strings.Index(out, "PHOTOPRISM_HTTP_CACHE_PUBLIC") + + if webServerHeading < 0 || httpCompression < 0 || httpHeaderTimeout < 0 || httpHeaderBytes < 0 || httpIdleTimeout < 0 || httpCachePublic < 0 { + t.Fatalf("expected web server heading and related rows in output") + } + + if httpHeaderTimeout < webServerHeading { + t.Fatalf("expected http-header-timeout row to appear in Web Server section") + } + + if httpHeaderTimeout < httpCompression { + t.Fatalf("expected http-header-timeout row to appear after http-compression") + } + + if httpHeaderBytes < httpHeaderTimeout { + t.Fatalf("expected http-header-bytes row to appear after http-header-timeout") + } + + if httpIdleTimeout < httpHeaderBytes { + t.Fatalf("expected http-idle-timeout row to appear after http-header-bytes") + } + + if httpIdleTimeout > httpCachePublic { + t.Fatalf("expected http-idle-timeout row to appear before http-cache-public") + } +} + +func TestShowConfigYaml_MarkdownHttpHeaderSettingsOrder(t *testing.T) { + out, err := RunWithTestContext(ShowConfigYamlCommand, []string{"config-yaml", "--md"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + webServerHeading := strings.Index(out, "### Web Server") + httpCompression := strings.Index(out, "HttpCompression") + httpHeaderTimeout := strings.Index(out, "HttpHeaderTimeout") + httpHeaderBytes := strings.Index(out, "HttpHeaderBytes") + httpIdleTimeout := strings.Index(out, "HttpIdleTimeout") + httpCachePublic := strings.Index(out, "HttpCachePublic") + + if webServerHeading < 0 || httpCompression < 0 || httpHeaderTimeout < 0 || httpHeaderBytes < 0 || httpIdleTimeout < 0 || httpCachePublic < 0 { + t.Fatalf("expected web server heading and related rows in output") + } + + if httpHeaderTimeout < webServerHeading { + t.Fatalf("expected HttpHeaderTimeout row to appear in Web Server section") + } + + if httpHeaderTimeout < httpCompression { + t.Fatalf("expected HttpHeaderTimeout row to appear after HttpCompression") + } + + if httpHeaderBytes < httpHeaderTimeout { + t.Fatalf("expected HttpHeaderBytes row to appear after HttpHeaderTimeout") + } + + if httpIdleTimeout < httpHeaderBytes { + t.Fatalf("expected HttpIdleTimeout row to appear after HttpHeaderBytes") + } + + if httpIdleTimeout > httpCachePublic { + t.Fatalf("expected HttpIdleTimeout row to appear before HttpCachePublic") + } +} + func TestShowFileFormats_JSON(t *testing.T) { out, err := RunWithTestContext(ShowFileFormatsCommand, []string{"file-formats", "--json"}) diff --git a/internal/commands/start.go b/internal/commands/start.go index 36ed7fad3..543262b09 100644 --- a/internal/commands/start.go +++ b/internal/commands/start.go @@ -65,6 +65,9 @@ func startAction(ctx *cli.Context) error { {"detach-server", fmt.Sprintf("%t", conf.DetachServer())}, {"http-mode", conf.HttpMode()}, {"http-compression", conf.HttpCompression()}, + {"http-header-timeout", conf.HttpHeaderTimeout().String()}, + {"http-header-bytes", fmt.Sprintf("%d", conf.HttpHeaderBytes())}, + {"http-idle-timeout", conf.HttpIdleTimeout().String()}, {"http-cache-public", fmt.Sprintf("%t", conf.HttpCachePublic())}, {"http-cache-maxage", fmt.Sprintf("%d", conf.HttpCacheMaxAge())}, {"http-video-maxage", fmt.Sprintf("%d", conf.HttpVideoMaxAge())}, diff --git a/internal/config/README.md b/internal/config/README.md index 0cb6a5d3d..df381fa83 100644 --- a/internal/config/README.md +++ b/internal/config/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — Config Package -**Last Updated:** March 2, 2026 +**Last Updated:** March 4, 2026 ### Overview @@ -30,6 +30,13 @@ The `PHOTOPRISM_CONFIG_PATH` variable controls where PhotoPrism looks for YAML f > Any change to configuration (flags, env vars, YAML files) requires a restart. The Go process reads options during startup and does not watch for changes. +### HTTP Hardening Defaults + +- `PHOTOPRISM_HTTP_HEADER_TIMEOUT` / `--http-header-timeout` configures `http.Server.ReadHeaderTimeout` and defaults to `15s`. +- `PHOTOPRISM_HTTP_HEADER_BYTES` / `--http-header-bytes` configures `http.Server.MaxHeaderBytes` and defaults to `1048576` (1 MiB). +- `PHOTOPRISM_HTTP_IDLE_TIMEOUT` / `--http-idle-timeout` configures `http.Server.IdleTimeout` and defaults to `180s`. +- `ReadTimeout` and `WriteTimeout` remain disabled globally so large uploads/downloads are not interrupted by a one-size-fits-all timeout. + ### Inspect Before Editing Before changing environment variables or YAML files, run `photoprism config | grep -i ` to confirm the current value of a flag, such as `site-url`, or `site` to show all related values: diff --git a/internal/config/config_const.go b/internal/config/config_const.go index 9f7f27204..3838dd329 100644 --- a/internal/config/config_const.go +++ b/internal/config/config_const.go @@ -44,6 +44,15 @@ const DefaultWakeupIntervalSeconds = int(15 * 60) // 15 Minutes // DefaultWakeupInterval is the default worker interval as a duration. const DefaultWakeupInterval = time.Second * time.Duration(DefaultWakeupIntervalSeconds) +// DefaultHttpHeaderTimeout is the default timeout for reading request headers. +const DefaultHttpHeaderTimeout = 15 * time.Second + +// DefaultHttpHeaderBytes is the default limit for HTTP request header size. +const DefaultHttpHeaderBytes = 1 << 20 // 1 MiB + +// DefaultHttpIdleTimeout is the default timeout for idle keep-alive connections. +const DefaultHttpIdleTimeout = 180 * time.Second + // MegaByte defines a megabyte in bytes. const MegaByte = 1000 * 1000 // 1,000,000 Bytes diff --git a/internal/config/config_server.go b/internal/config/config_server.go index 67d5d6fb6..e13cafe1f 100644 --- a/internal/config/config_server.go +++ b/internal/config/config_server.go @@ -5,6 +5,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "github.com/photoprism/photoprism/internal/config/ttl" "github.com/photoprism/photoprism/internal/server/limiter" @@ -103,6 +104,33 @@ func (c *Config) HttpCompression() string { return strings.ToLower(strings.TrimSpace(c.options.HttpCompression)) } +// HttpHeaderTimeout returns the timeout for reading HTTP request headers. +func (c *Config) HttpHeaderTimeout() time.Duration { + if c.options.HttpHeaderTimeout <= 0 { + return DefaultHttpHeaderTimeout + } + + return c.options.HttpHeaderTimeout +} + +// HttpHeaderBytes returns the maximum size of HTTP request headers in bytes. +func (c *Config) HttpHeaderBytes() int { + if c.options.HttpHeaderBytes <= 0 { + return DefaultHttpHeaderBytes + } + + return c.options.HttpHeaderBytes +} + +// HttpIdleTimeout returns the timeout for idle keep-alive connections. +func (c *Config) HttpIdleTimeout() time.Duration { + if c.options.HttpIdleTimeout <= 0 { + return DefaultHttpIdleTimeout + } + + return c.options.HttpIdleTimeout +} + // HttpCachePublic checks whether static content may be cached by a CDN or caching proxy. func (c *Config) HttpCachePublic() bool { if c.options.HttpCachePublic { diff --git a/internal/config/config_server_test.go b/internal/config/config_server_test.go index c87c18c76..92772875b 100644 --- a/internal/config/config_server_test.go +++ b/internal/config/config_server_test.go @@ -3,6 +3,7 @@ package config import ( "os" "testing" + "time" "github.com/stretchr/testify/assert" @@ -118,6 +119,42 @@ func TestConfig_HttpCompression(t *testing.T) { assert.Equal(t, "", c.HttpCompression()) } +func TestConfig_HttpHeaderTimeout(t *testing.T) { + c := NewConfig(CliTestContext()) + + assert.Equal(t, DefaultHttpHeaderTimeout, c.HttpHeaderTimeout()) + + c.Options().HttpHeaderTimeout = 17 * time.Second + assert.Equal(t, 17*time.Second, c.HttpHeaderTimeout()) + + c.Options().HttpHeaderTimeout = -1 * time.Second + assert.Equal(t, DefaultHttpHeaderTimeout, c.HttpHeaderTimeout()) +} + +func TestConfig_HttpHeaderBytes(t *testing.T) { + c := NewConfig(CliTestContext()) + + assert.Equal(t, DefaultHttpHeaderBytes, c.HttpHeaderBytes()) + + c.Options().HttpHeaderBytes = 2048 + assert.Equal(t, 2048, c.HttpHeaderBytes()) + + c.Options().HttpHeaderBytes = 0 + assert.Equal(t, DefaultHttpHeaderBytes, c.HttpHeaderBytes()) +} + +func TestConfig_HttpIdleTimeout(t *testing.T) { + c := NewConfig(CliTestContext()) + + assert.Equal(t, DefaultHttpIdleTimeout, c.HttpIdleTimeout()) + + c.Options().HttpIdleTimeout = 2 * time.Minute + assert.Equal(t, 2*time.Minute, c.HttpIdleTimeout()) + + c.Options().HttpIdleTimeout = -1 * time.Second + assert.Equal(t, DefaultHttpIdleTimeout, c.HttpIdleTimeout()) +} + func TestConfig_HttpCachePublic(t *testing.T) { c := NewConfig(CliTestContext()) diff --git a/internal/config/flags.go b/internal/config/flags.go index 4335f061c..3247918bb 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -863,6 +863,24 @@ var Flags = CliFlags{ Usage: "Web server compression `METHOD` (gzip, none)", EnvVars: EnvVars("HTTP_COMPRESSION"), }}, { + Flag: &cli.StringFlag{ + Name: "http-header-timeout", + Usage: "timeout for reading request headers as `DURATION`", + Value: DefaultHttpHeaderTimeout.String(), + EnvVars: EnvVars("HTTP_HEADER_TIMEOUT"), + }}, { + Flag: &cli.IntFlag{ + Name: "http-header-bytes", + Usage: "maximum request header size in `BYTES`", + Value: DefaultHttpHeaderBytes, + EnvVars: EnvVars("HTTP_HEADER_BYTES"), + }}, { + Flag: &cli.StringFlag{ + Name: "http-idle-timeout", + Usage: "timeout for idle keep-alive connections as `DURATION`", + Value: DefaultHttpIdleTimeout.String(), + EnvVars: EnvVars("HTTP_IDLE_TIMEOUT"), + }}, { Flag: &cli.BoolFlag{ Name: "http-cache-public", Usage: "allows static content to be cached by a CDN or caching proxy", diff --git a/internal/config/options.go b/internal/config/options.go index c2c67e974..11c61ae8f 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -180,6 +180,9 @@ type Options struct { TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"` HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"` HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"` + HttpHeaderTimeout time.Duration `yaml:"HttpHeaderTimeout" json:"-" flag:"http-header-timeout"` + HttpHeaderBytes int `yaml:"HttpHeaderBytes" json:"-" flag:"http-header-bytes"` + HttpIdleTimeout time.Duration `yaml:"HttpIdleTimeout" json:"-" flag:"http-idle-timeout"` HttpCachePublic bool `yaml:"HttpCachePublic" json:"HttpCachePublic" flag:"http-cache-public"` HttpCacheMaxAge int `yaml:"HttpCacheMaxAge" json:"HttpCacheMaxAge" flag:"http-cache-maxage"` HttpVideoMaxAge int `yaml:"HttpVideoMaxAge" json:"HttpVideoMaxAge" flag:"http-video-maxage"` diff --git a/internal/config/report.go b/internal/config/report.go index d7a1c91c3..1965ca530 100644 --- a/internal/config/report.go +++ b/internal/config/report.go @@ -233,6 +233,9 @@ func (c *Config) Report() (rows [][]string, cols []string) { {"tls-key", c.TLSKey()}, {"http-mode", c.HttpMode()}, {"http-compression", c.HttpCompression()}, + {"http-header-timeout", c.HttpHeaderTimeout().String()}, + {"http-header-bytes", fmt.Sprintf("%d", c.HttpHeaderBytes())}, + {"http-idle-timeout", c.HttpIdleTimeout().String()}, {"http-cache-public", fmt.Sprintf("%t", c.HttpCachePublic())}, {"http-cache-maxage", fmt.Sprintf("%d", c.HttpCacheMaxAge())}, {"http-video-maxage", fmt.Sprintf("%d", c.HttpVideoMaxAge())}, diff --git a/internal/config/report_test.go b/internal/config/report_test.go index 139cdaf2c..406c390d6 100644 --- a/internal/config/report_test.go +++ b/internal/config/report_test.go @@ -52,6 +52,37 @@ func TestConfig_ReportServicesCIDROrder(t *testing.T) { assert.Less(t, servicesCIDR, disableTLS) } +func TestConfig_ReportHttpHeaderSettingsOrder(t *testing.T) { + conf := NewConfig(CliTestContext()) + rows, _ := conf.Report() + + indexOf := func(name string) int { + for i := range rows { + if len(rows[i]) > 0 && rows[i][0] == name { + return i + } + } + + return -1 + } + + httpCompression := indexOf("http-compression") + httpHeaderTimeout := indexOf("http-header-timeout") + httpHeaderBytes := indexOf("http-header-bytes") + httpIdleTimeout := indexOf("http-idle-timeout") + httpCachePublic := indexOf("http-cache-public") + + assert.Greater(t, httpCompression, -1) + assert.Greater(t, httpHeaderTimeout, -1) + assert.Greater(t, httpHeaderBytes, -1) + assert.Greater(t, httpIdleTimeout, -1) + assert.Greater(t, httpCachePublic, -1) + assert.Greater(t, httpHeaderTimeout, httpCompression) + assert.Greater(t, httpHeaderBytes, httpHeaderTimeout) + assert.Greater(t, httpIdleTimeout, httpHeaderBytes) + assert.Less(t, httpIdleTimeout, httpCachePublic) +} + func TestConfig_ReportDatabaseSection(t *testing.T) { collect := func(rows [][]string) map[string]string { result := make(map[string]string, len(rows)) diff --git a/internal/server/README.md b/internal/server/README.md index 711dafed7..5e1c0787e 100644 --- a/internal/server/README.md +++ b/internal/server/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — HTTP Server -**Last Updated:** February 26, 2026 +**Last Updated:** March 4, 2026 ### Overview @@ -17,7 +17,7 @@ - Provide a single entrypoint (`Start`) that configures listeners, middleware, and routes consistently. - Keep health/readiness endpoints lightweight and cache-safe. -- Ensure redirect and TLS listeners include sensible timeouts. +- Ensure redirect and TLS listeners include sensible header and idle limits. #### Non-Goals @@ -46,7 +46,12 @@ - Compression: only gzip is enabled; brotli requests log a notice. - Trusted proxies/platform headers are read from config; keep the list tight. - If no trusted proxy ranges are configured (or the configured ranges are invalid), proxy trust is disabled and client IP resolution falls back to the TCP peer address. -- AutoTLS: uses `autocert` and spins up a redirect listener with explicit read/write timeouts; ensure ports 80/443 are reachable. +- HTTP hardening defaults: + - `ReadHeaderTimeout` is configured via `PHOTOPRISM_HTTP_HEADER_TIMEOUT` / `--http-header-timeout` (default `15s`). + - `MaxHeaderBytes` is configured via `PHOTOPRISM_HTTP_HEADER_BYTES` / `--http-header-bytes` (default `1 MiB`). + - `IdleTimeout` is configured via `PHOTOPRISM_HTTP_IDLE_TIMEOUT` / `--http-idle-timeout` (default `180s`). + - Global `ReadTimeout` / `WriteTimeout` remain disabled to avoid breaking large transfers. +- AutoTLS: uses `autocert` and spins up a redirect listener; ensure ports 80/443 are reachable. - Unix sockets: optional `force` query removes stale sockets; permissions can be set via `mode` query. - Health endpoints (`/livez`, `/health`, `/healthz`, `/readyz`) return `Cache-Control: no-store` and `Access-Control-Allow-Origin: *`. diff --git a/internal/server/start.go b/internal/server/start.go index f5c521af9..45b2f4a45 100644 --- a/internal/server/start.go +++ b/internal/server/start.go @@ -117,17 +117,9 @@ func Start(ctx context.Context, conf *config.Config) { } router.Any(conf.BaseUri("/readyz"), isReady) - // Create a new HTTP server instance with no read or write timeout, except for reading the headers: - // https://pkg.go.dev/net/http#Server - server := &http.Server{ - ReadHeaderTimeout: time.Minute, - ReadTimeout: -1, - WriteTimeout: -1, - Handler: router, - } - var tlsErr error var tlsManager *autocert.Manager + var server *http.Server // Listen on a Unix domain socket instead of a TCP port? if unixSocket := conf.HttpSocket(); unixSocket != nil { @@ -168,6 +160,7 @@ func Start(ctx context.Context, conf *config.Config) { // Listen on Unix socket, which should be automatically closed and removed after use: // https://pkg.go.dev/net#UnixListener.SetUnlinkOnClose. + server = newHTTPServer(router, conf) server.Addr = listener.Addr().String() log.Infof("server: listening on %s [%s]", unixSocket.Path, time.Since(start)) @@ -182,6 +175,8 @@ func Start(ctx context.Context, conf *config.Config) { tlsConfig := tlsManager.TLSConfig() tlsConfig.MinVersion = tls.VersionTLS12 + server = newHTTPServer(router, conf) + // Listen on HTTPS socket. server.Addr = tlsSocket server.TLSConfig = tlsConfig @@ -198,6 +193,8 @@ func Start(ctx context.Context, conf *config.Config) { MinVersion: tls.VersionTLS12, } + server = newHTTPServer(router, conf) + // Listen on HTTPS socket. server.Addr = tlsSocket server.TLSConfig = tlsConfig @@ -216,6 +213,7 @@ func Start(ctx context.Context, conf *config.Config) { return } else { // Listen on HTTP socket. + server = newHTTPServer(router, conf) server.Addr = tcpSocket log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start)) @@ -281,13 +279,8 @@ func StartAutoTLS(s *http.Server, m *autocert.Manager, conf *config.Config) { var g errgroup.Group g.Go(func() error { - redirectSrv := &http.Server{ - Addr: fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()), - Handler: m.HTTPHandler(http.HandlerFunc(redirect)), - ReadHeaderTimeout: time.Minute, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } + redirectSrv := newHTTPServer(m.HTTPHandler(http.HandlerFunc(redirect)), conf) + redirectSrv.Addr = fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()) return redirectSrv.ListenAndServe() }) @@ -310,3 +303,25 @@ func redirect(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, target, httpsRedirect) } + +// newHTTPServer creates an HTTP server with hardened header and idle settings. +func newHTTPServer(handler http.Handler, conf *config.Config) *http.Server { + headerTimeout := config.DefaultHttpHeaderTimeout + headerBytes := config.DefaultHttpHeaderBytes + idleTimeout := config.DefaultHttpIdleTimeout + + if conf != nil { + headerTimeout = conf.HttpHeaderTimeout() + headerBytes = conf.HttpHeaderBytes() + idleTimeout = conf.HttpIdleTimeout() + } + + return &http.Server{ + ReadHeaderTimeout: headerTimeout, + ReadTimeout: 0, + WriteTimeout: 0, + IdleTimeout: idleTimeout, + MaxHeaderBytes: headerBytes, + Handler: handler, + } +} diff --git a/internal/server/start_test.go b/internal/server/start_test.go index f61b88c58..61602fe5b 100644 --- a/internal/server/start_test.go +++ b/internal/server/start_test.go @@ -4,6 +4,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" @@ -77,3 +78,29 @@ func TestConfigureTrustedProxySettings(t *testing.T) { assert.Equal(t, "198.51.100.11", ip) }) } + +func TestNewHTTPServer(t *testing.T) { + t.Run("UsesConfiguredValues", func(t *testing.T) { + conf := config.NewConfig(config.CliTestContext()) + conf.Options().HttpHeaderTimeout = 15 * time.Second + conf.Options().HttpHeaderBytes = 2048 + conf.Options().HttpIdleTimeout = 2 * time.Minute + + server := newHTTPServer(http.NewServeMux(), conf) + + assert.Equal(t, 15*time.Second, server.ReadHeaderTimeout) + assert.Equal(t, 0*time.Second, server.ReadTimeout) + assert.Equal(t, 0*time.Second, server.WriteTimeout) + assert.Equal(t, 2*time.Minute, server.IdleTimeout) + assert.Equal(t, 2048, server.MaxHeaderBytes) + }) + t.Run("UsesDefaultsWhenConfigIsNil", func(t *testing.T) { + server := newHTTPServer(http.NewServeMux(), nil) + + assert.Equal(t, config.DefaultHttpHeaderTimeout, server.ReadHeaderTimeout) + assert.Equal(t, 0*time.Second, server.ReadTimeout) + assert.Equal(t, 0*time.Second, server.WriteTimeout) + assert.Equal(t, config.DefaultHttpIdleTimeout, server.IdleTimeout) + assert.Equal(t, config.DefaultHttpHeaderBytes, server.MaxHeaderBytes) + }) +}