Enhance environment variable documentation and configuration
Format / gofmt (push) Successful in 5s
CI / Build (push) Successful in 12s
CI / Go Tests (push) Successful in 22s
Release Artifacts / Validate release tag (push) Successful in 1s
Release Artifacts / Build and release executables (push) Successful in 33s
Release Artifacts / Build and release Docker image (push) Successful in 3m10s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-15 16:27:18 -05:00
parent ad4355df17
commit 90f1cf8bdf
11 changed files with 271 additions and 16 deletions
+67 -6
View File
@@ -49,6 +49,62 @@ const (
envPrefix = "SCRATCHBOX_"
)
// envSuffixes is the single source of truth for every SCRATCHBOX_* environment
// variable suffix supported by applyEnvOverrides (without the "SCRATCHBOX_" prefix).
//
// Usage sites that hardcode or document these vars MUST have a comment pointing here:
// - applyEnvOverrides (this file)
// - printEnvStartup + flag docs in cmd/scratchbox/server.go
// - docker-entrypoint.sh
// - .env.example
// - README.md (supported list + example block)
// - tests that setenv them (e.g. config_test.go, main_test.go)
//
// When you add, remove, or rename support in applyEnvOverrides (or the corresponding
// struct fields), you MUST:
// 1. Update this list.
// 2. Add/update the pointer comment at every usage site.
// 3. Update .env.example with the new var (plus a sensible default + comment).
// 4. Update the "Environment variables supported by..." list in README.md.
// 5. Update the example .env block in README.md.
//
// A test in config_test.go will fail if .env.example or README.md are missing any
// entry from this list. This prevents the documentation drift that has happened
// in the past when new config fields (e.g. shutdown_timeout, access_log details)
// were added.
var envSuffixes = []string{
"SERVER_LISTEN_ADDR",
"SERVER_READ_HEADER_TIMEOUT",
"SERVER_READ_TIMEOUT",
"SERVER_WRITE_TIMEOUT",
"SERVER_IDLE_TIMEOUT",
"SERVER_SHUTDOWN_TIMEOUT",
"SERVER_MAX_HEADER_BYTES",
"SERVER_ACCESS_LOG_ENABLED",
"SERVER_ACCESS_LOG_FILE_PATH",
"SERVER_ACCESS_LOG_MAX_SIZE",
"SERVER_ACCESS_LOG_MAX_BACKUPS",
"LIMITS_MAX_UPLOAD_SIZE",
"LIMITS_DEFAULT_TTL",
"LIMITS_RAW_CACHE_MAX_SIZE",
"LIMITS_RAW_CACHE_MAX_ENTRIES",
"STORAGE_DATA_DIR",
"STORAGE_CLEANUP_INTERVAL",
"SECURITY_ALLOWED_IPS",
"SECURITY_TRUST_PROXY_HEADERS",
"SECURITY_TRUSTED_PROXY_IPS",
"SECURITY_RATE_LIMIT_UI_ENABLED",
"SECURITY_RATE_LIMIT_UI_REQUESTS_PER_MINUTE",
"SECURITY_RATE_LIMIT_UI_BURST",
"SECURITY_RATE_LIMIT_API_READ_ENABLED",
"SECURITY_RATE_LIMIT_API_READ_REQUESTS_PER_MINUTE",
"SECURITY_RATE_LIMIT_API_READ_BURST",
"SECURITY_RATE_LIMIT_API_WRITE_ENABLED",
"SECURITY_RATE_LIMIT_API_WRITE_REQUESTS_PER_MINUTE",
"SECURITY_RATE_LIMIT_API_WRITE_BURST",
"SECURITY_HSTS_ENABLED",
}
type Config struct {
Server ServerConfig `yaml:"server" comment:"HTTP listener settings."`
Limits LimitsConfig `yaml:"limits" comment:"Upload, expiration, and raw cache defaults."`
@@ -59,8 +115,8 @@ type Config struct {
type ServerConfig struct {
ListenAddr string `yaml:"listen_addr" comment:"Address to bind the HTTP server to."`
ReadHeaderTimeout string `yaml:"read_header_timeout" comment:"Maximum duration for reading request headers (for slowloris protection). Must be > 0."`
ReadTimeout string `yaml:"read_timeout" comment:"Maximum duration for reading the full request, including body. Must be > 0."`
WriteTimeout string `yaml:"write_timeout" comment:"Maximum duration before timing out writes of a response. Must be > 0."`
ReadTimeout string `yaml:"read_timeout" comment:"Maximum duration for reading the full request, including body. Use 0 to disable (recommended when limits.max_upload_size is multi-GiB or clients are slow). Must be >= 0."`
WriteTimeout string `yaml:"write_timeout" comment:"Maximum duration before timing out writes of a response (also bounds slow handlers after headers are read). Use 0 to disable (recommended for large uploads/downloads). Must be >= 0."`
IdleTimeout string `yaml:"idle_timeout" comment:"Maximum amount of time to wait for the next request when keep-alives are enabled. Must be > 0."`
ShutdownTimeout string `yaml:"shutdown_timeout" comment:"Graceful shutdown timeout for the HTTP server. Must be > 0."`
MaxHeaderBytes int `yaml:"max_header_bytes" comment:"Maximum size of request headers in bytes. Must be > 0."`
@@ -214,8 +270,9 @@ func (c *Config) Validate() error {
if err != nil {
return fmt.Errorf("server.read_timeout invalid: %w", err)
}
if readTimeout <= 0 {
return errors.New("server.read_timeout must be > 0")
// 0 means no timeout (matches net/http.Server.ReadTimeout). Negative is invalid.
if readTimeout < 0 {
return errors.New("server.read_timeout must be >= 0")
}
c.Server.ReadTimeoutDur = readTimeout
@@ -223,8 +280,9 @@ func (c *Config) Validate() error {
if err != nil {
return fmt.Errorf("server.write_timeout invalid: %w", err)
}
if writeTimeout <= 0 {
return errors.New("server.write_timeout must be > 0")
// 0 means no timeout (matches net/http.Server.WriteTimeout). Negative is invalid.
if writeTimeout < 0 {
return errors.New("server.write_timeout must be >= 0")
}
c.Server.WriteTimeoutDur = writeTimeout
@@ -530,6 +588,9 @@ func decodeEncryptionKey(raw string) ([]byte, error) {
return encKey, nil
}
// applyEnvOverrides applies all supported SCRATCHBOX_* vars.
// The canonical list of supported suffixes is in envSuffixes (defined earlier in this file).
// See the comment on envSuffixes for the full maintenance rules (update list + .env.example + README).
func (c *Config) applyEnvOverrides(prefix string) error {
if err := applyStringEnv(prefix+"SERVER_LISTEN_ADDR", &c.Server.ListenAddr); err != nil {
return err