From cdc4f9a3eb12f9bc4017b2c4b96fa9ba0cdeb2a0 Mon Sep 17 00:00:00 2001 From: "Andre H. Beckedorf" Date: Sat, 11 Jul 2026 17:35:40 +0200 Subject: [PATCH 1/2] feat: replace several hard-coded timeout values with new optional config values in order to support very long-running requests --- README.md | 4 ++++ config.toml | 4 ++++ main.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6548dcf..4623c11 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ The service is configured using a TOML file. Here's an example configuration: ```toml port = ":8080" # Port to listen on timeout = "1m" # How long to wait for server to wake up +request_header_timeout = "30s" # How long to wait for request headers response_header_timeout = "1m" # How long to wait for a response header, e.g. during or after slow or long-running requests/uploads +server_read_timeout = "10m" # How long to wait for the full request body from the client +server_write_timeout = "10m" # How long to wait for the full response to the client +server_idle_timeout = "120s" # How long to wait for the next request when keep-alive is enabled poll_interval = "5s" # How often to check health during wake-up health_check_interval = "30s" # Background health check frequency health_cache_duration = "10s" # How long to trust cached health status diff --git a/config.toml b/config.toml index c8a2106..2fc3e3d 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,10 @@ port = ":8080" # Port to listen on timeout = "1m" # How long to wait for server to wake up +request_header_timeout = "30s" # How long to wait for request headers response_header_timeout = "1m" # How long to wait for a response header, e.g. during or after slow or long-running requests/uploads +server_read_timeout = "10m" # How long to wait for the full request body from the client +server_write_timeout = "10m" # How long to wait for the full response to the client +server_idle_timeout = "120s" # How long to wait for the next request when keep-alive is enabled poll_interval = "5s" # How often to check health during wake-up health_check_interval = "30s" # Background health check frequency health_cache_duration = "10s" # How long to trust cached health status diff --git a/main.go b/main.go index 2f9fbfb..75c1413 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,11 @@ type Logger interface { type Config struct { Port string `toml:"port"` Timeout string `toml:"timeout"` + RequestHeaderTimeout string `toml:"request_header_timeout"` ResponseHeaderTimeout string `toml:"response_header_timeout"` + ServerReadTimeout string `toml:"server_read_timeout"` + ServerWriteTimeout string `toml:"server_write_timeout"` + ServerIdleTimeout string `toml:"server_idle_timeout"` PollInterval string `toml:"poll_interval"` HealthCheckInterval string `toml:"health_check_interval"` HealthCacheDuration string `toml:"health_cache_duration"` @@ -75,7 +79,11 @@ type Target struct { type ProxyConfig struct { Port string Timeout time.Duration + RequestHeaderTimeout time.Duration ResponseHeaderTimeout time.Duration + ServerReadTimeout time.Duration + ServerWriteTimeout time.Duration + ServerIdleTimeout time.Duration PollInterval time.Duration HealthCheckInterval time.Duration HealthCacheDuration time.Duration @@ -517,10 +525,10 @@ func (p *ProxyService) Start(ctx context.Context) error { server := &http.Server{ Addr: p.config.Port, Handler: mux, - ReadTimeout: 10 * time.Minute, - WriteTimeout: 10 * time.Minute, - IdleTimeout: 120 * time.Second, // 2 minutes for keep-alive connections - ReadHeaderTimeout: 30 * time.Second, + ReadTimeout: p.config.ServerReadTimeout, + WriteTimeout: p.config.ServerWriteTimeout, + IdleTimeout: p.config.ServerIdleTimeout, + ReadHeaderTimeout: p.config.RequestHeaderTimeout, MaxHeaderBytes: 1 << 20, } @@ -816,6 +824,15 @@ func LoadConfig(filename string) (*ProxyConfig, error) { return nil, fmt.Errorf("invalid timeout: %w", err) } + if config.RequestHeaderTimeout == "" { + config.RequestHeaderTimeout = "30s" + } + requestHeaderTimeout, err := time.ParseDuration(config.RequestHeaderTimeout) + if err != nil { + return nil, fmt.Errorf("invalid request_header_timeout: %w", err) + } + + if config.ResponseHeaderTimeout == "" { config.ResponseHeaderTimeout = "1m" } @@ -824,6 +841,30 @@ func LoadConfig(filename string) (*ProxyConfig, error) { return nil, fmt.Errorf("invalid response_header_timeout: %w", err) } + if config.ServerReadTimeout == "" { + config.ServerReadTimeout = "10m" + } + serverReadTimeout, err := time.ParseDuration(config.ServerReadTimeout) + if err != nil { + return nil, fmt.Errorf("invalid server_read_timeout: %w", err) + } + + if config.ServerWriteTimeout == "" { + config.ServerWriteTimeout = "10m" + } + serverWriteTimeout, err := time.ParseDuration(config.ServerWriteTimeout) + if err != nil { + return nil, fmt.Errorf("invalid server_write_timeout: %w", err) + } + + if config.ServerIdleTimeout == "" { + config.ServerIdleTimeout = "120s" + } + serverIdleTimeout, err := time.ParseDuration(config.ServerIdleTimeout) + if err != nil { + return nil, fmt.Errorf("invalid server_idle_timeout: %w", err) + } + pollInterval, err := time.ParseDuration(config.PollInterval) if err != nil { return nil, fmt.Errorf("invalid poll_interval: %w", err) @@ -888,6 +929,10 @@ func LoadConfig(filename string) (*ProxyConfig, error) { SSLCertificateKey: config.SSLCertificateKey, Timeout: timeout, ResponseHeaderTimeout: responseHeaderTimeout, + ServerReadTimeout: serverReadTimeout, + ServerWriteTimeout: serverWriteTimeout, + ServerIdleTimeout: serverIdleTimeout, + RequestHeaderTimeout: requestHeaderTimeout, PollInterval: pollInterval, HealthCheckInterval: healthCheckInterval, HealthCacheDuration: healthCacheDuration, From 36902c79c50e8fdbc847464972ad9cacd0bbacdd Mon Sep 17 00:00:00 2001 From: "Andre H. Beckedorf" Date: Sat, 11 Jul 2026 20:23:34 +0200 Subject: [PATCH 2/2] chore: simplified the duration parsing logic and introduced new Durations struct and default value tag in Config struct. --- main.go | 140 ++++++++++++++++++++++---------------------------------- 1 file changed, 54 insertions(+), 86 deletions(-) diff --git a/main.go b/main.go index 75c1413..f8477c1 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "reflect" "crypto/tls" "fmt" "io/ioutil" @@ -45,11 +46,11 @@ type Logger interface { type Config struct { Port string `toml:"port"` Timeout string `toml:"timeout"` - RequestHeaderTimeout string `toml:"request_header_timeout"` - ResponseHeaderTimeout string `toml:"response_header_timeout"` - ServerReadTimeout string `toml:"server_read_timeout"` - ServerWriteTimeout string `toml:"server_write_timeout"` - ServerIdleTimeout string `toml:"server_idle_timeout"` + RequestHeaderTimeout string `toml:"request_header_timeout" default:"30s"` + ResponseHeaderTimeout string `toml:"response_header_timeout" default:"1m"` + ServerReadTimeout string `toml:"server_read_timeout" default:"10m"` + ServerWriteTimeout string `toml:"server_write_timeout" default:"10m"` + ServerIdleTimeout string `toml:"server_idle_timeout" default:"120s"` PollInterval string `toml:"poll_interval"` HealthCheckInterval string `toml:"health_check_interval"` HealthCacheDuration string `toml:"health_cache_duration"` @@ -76,8 +77,7 @@ type Target struct { InactivityThreshold string `toml:"inactivity_threshold"` } -type ProxyConfig struct { - Port string +type Durations struct { Timeout time.Duration RequestHeaderTimeout time.Duration ResponseHeaderTimeout time.Duration @@ -87,6 +87,11 @@ type ProxyConfig struct { PollInterval time.Duration HealthCheckInterval time.Duration HealthCacheDuration time.Duration +} + +type ProxyConfig struct { + Port string + Durations Durations Targets map[string]*TargetState HostnameMap map[string]string // hostname -> target name InactivityThresholds map[string]time.Duration // target name -> inactivity threshold @@ -498,7 +503,7 @@ func (p *ProxyService) Start(ctx context.Context) error { p.healthChecker.StartBackgroundChecks( ctx, p.config.Targets, - p.config.HealthCheckInterval, + p.config.Durations.HealthCheckInterval, ) // Wait for initial health checks to complete @@ -525,10 +530,10 @@ func (p *ProxyService) Start(ctx context.Context) error { server := &http.Server{ Addr: p.config.Port, Handler: mux, - ReadTimeout: p.config.ServerReadTimeout, - WriteTimeout: p.config.ServerWriteTimeout, - IdleTimeout: p.config.ServerIdleTimeout, - ReadHeaderTimeout: p.config.RequestHeaderTimeout, + ReadTimeout: p.config.Durations.ServerReadTimeout, + WriteTimeout: p.config.Durations.ServerWriteTimeout, + IdleTimeout: p.config.Durations.ServerIdleTimeout, + ReadHeaderTimeout: p.config.Durations.RequestHeaderTimeout, MaxHeaderBytes: 1 << 20, } @@ -624,10 +629,10 @@ func (p *ProxyService) healthCacheStatus(target *TargetState) (cached bool, reas } age := time.Since(target.LastCheck) - if age > p.config.HealthCacheDuration { + if age > p.config.Durations.HealthCacheDuration { return false, fmt.Sprintf( "cached health expired (last check %v ago, cache duration %v)", - age.Round(time.Second), p.config.HealthCacheDuration, + age.Round(time.Second), p.config.Durations.HealthCacheDuration, ) } @@ -668,8 +673,8 @@ func (p *ProxyService) wakeAndWait(ctx context.Context, target *TargetState) err } func (p *ProxyService) waitForWake(ctx context.Context, target *TargetState) error { - timeout := time.After(p.config.Timeout) - healthCheckTicker := time.NewTicker(p.config.PollInterval) + timeout := time.After(p.config.Durations.Timeout) + healthCheckTicker := time.NewTicker(p.config.Durations.PollInterval) defer healthCheckTicker.Stop() // Create a separate ticker for sending WOL packets @@ -679,7 +684,7 @@ func (p *ProxyService) waitForWake(ctx context.Context, target *TargetState) err wakeStartTime := time.Now() p.logger.Info("Waiting for %s (%s) to wake (poll interval %v, timeout %v)", - target.Target.Name, target.Target.Hostname, p.config.PollInterval, p.config.Timeout) + target.Target.Name, target.Target.Hostname, p.config.Durations.PollInterval, p.config.Durations.Timeout) for { select { @@ -690,7 +695,7 @@ func (p *ProxyService) waitForWake(ctx context.Context, target *TargetState) err target.IsWaking = false target.mu.Unlock() return fmt.Errorf("timeout waiting for %s to wake up after %v", - target.Target.Name, p.config.Timeout) + target.Target.Name, p.config.Durations.Timeout) case <-wolTicker.C: // Send additional WOL packets while waiting err := p.wolSender.SendWOL( @@ -758,7 +763,7 @@ func (p *ProxyService) proxyRequest(w http.ResponseWriter, r *http.Request, targ MaxIdleConnsPerHost: 10, // Disable compression to avoid issues with already compressed data DisableCompression: true, - ResponseHeaderTimeout: p.config.ResponseHeaderTimeout, + ResponseHeaderTimeout: p.config.Durations.ResponseHeaderTimeout, // No timeout for reading the entire response ReadBufferSize: 1024 * 1024, // 1MB buffer for reading WriteBufferSize: 1024 * 1024, // 1MB buffer for writing @@ -799,6 +804,33 @@ func (p *ProxyService) proxyRequest(w http.ResponseWriter, r *http.Request, targ proxy.ServeHTTP(w, r) } +func parseDurations(c Config) (Durations, error) { + var d Durations + configVal := reflect.ValueOf(c) + durationsVal := reflect.ValueOf(&d).Elem() + for i := 0; i < configVal.NumField(); i++ { + configField := configVal.Type().Field(i) + tag := configField.Tag.Get("toml") + if tag == "" { + continue + } + durationField := durationsVal.FieldByName(configField.Name) + if !durationField.CanSet() { + continue + } + v := configVal.Field(i).String() + if v == "" { + v = configField.Tag.Get("default") + } + dur, err := time.ParseDuration(v) + if err != nil { + return Durations{}, fmt.Errorf("invalid %s: %w", tag, err) + } + durationField.Set(reflect.ValueOf(dur)) + } + return d, nil +} + // Config loader func LoadConfig(filename string) (*ProxyConfig, error) { var config Config @@ -819,65 +851,9 @@ func LoadConfig(filename string) (*ProxyConfig, error) { config.Port = ":" + config.Port } - timeout, err := time.ParseDuration(config.Timeout) - if err != nil { - return nil, fmt.Errorf("invalid timeout: %w", err) - } - - if config.RequestHeaderTimeout == "" { - config.RequestHeaderTimeout = "30s" - } - requestHeaderTimeout, err := time.ParseDuration(config.RequestHeaderTimeout) + durations, err := parseDurations(config) if err != nil { - return nil, fmt.Errorf("invalid request_header_timeout: %w", err) - } - - - if config.ResponseHeaderTimeout == "" { - config.ResponseHeaderTimeout = "1m" - } - responseHeaderTimeout, err := time.ParseDuration(config.ResponseHeaderTimeout) - if err != nil { - return nil, fmt.Errorf("invalid response_header_timeout: %w", err) - } - - if config.ServerReadTimeout == "" { - config.ServerReadTimeout = "10m" - } - serverReadTimeout, err := time.ParseDuration(config.ServerReadTimeout) - if err != nil { - return nil, fmt.Errorf("invalid server_read_timeout: %w", err) - } - - if config.ServerWriteTimeout == "" { - config.ServerWriteTimeout = "10m" - } - serverWriteTimeout, err := time.ParseDuration(config.ServerWriteTimeout) - if err != nil { - return nil, fmt.Errorf("invalid server_write_timeout: %w", err) - } - - if config.ServerIdleTimeout == "" { - config.ServerIdleTimeout = "120s" - } - serverIdleTimeout, err := time.ParseDuration(config.ServerIdleTimeout) - if err != nil { - return nil, fmt.Errorf("invalid server_idle_timeout: %w", err) - } - - pollInterval, err := time.ParseDuration(config.PollInterval) - if err != nil { - return nil, fmt.Errorf("invalid poll_interval: %w", err) - } - - healthCheckInterval, err := time.ParseDuration(config.HealthCheckInterval) - if err != nil { - return nil, fmt.Errorf("invalid health_check_interval: %w", err) - } - - healthCacheDuration, err := time.ParseDuration(config.HealthCacheDuration) - if err != nil { - return nil, fmt.Errorf("invalid health_cache_duration: %w", err) + return nil, err } targets := make(map[string]*TargetState) @@ -927,15 +903,7 @@ func LoadConfig(filename string) (*ProxyConfig, error) { Port: config.Port, SSLCertificate: config.SSLCertificate, SSLCertificateKey: config.SSLCertificateKey, - Timeout: timeout, - ResponseHeaderTimeout: responseHeaderTimeout, - ServerReadTimeout: serverReadTimeout, - ServerWriteTimeout: serverWriteTimeout, - ServerIdleTimeout: serverIdleTimeout, - RequestHeaderTimeout: requestHeaderTimeout, - PollInterval: pollInterval, - HealthCheckInterval: healthCheckInterval, - HealthCacheDuration: healthCacheDuration, + Durations: durations, Targets: targets, HostnameMap: hostnameMap, InactivityThresholds: inactivityThresholds,