From 1d8f95e66ff7b8d76fec972e6e0215041bdae0c4 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Thu, 27 Aug 2026 16:21:43 -0400 Subject: [PATCH] Report configuration errors instead of printing them - Collect environment parse failures during initialisation. - Fail startup when any were recorded. --- internal/core/config.go | 23 +++++++++++++-- internal/core/config_test.go | 56 ++++++++++++++++++++++++++++++++++++ internal/core/encryption.go | 7 ++--- internal/core/image.go | 5 +--- internal/source/file.go | 5 +--- internal/source/s3.go | 5 +--- 6 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 internal/core/config_test.go diff --git a/internal/core/config.go b/internal/core/config.go index 8a59fa6..eb126de 100644 --- a/internal/core/config.go +++ b/internal/core/config.go @@ -1,6 +1,7 @@ package core import ( + "errors" "fmt" "github.com/caarlos0/env/v10" @@ -137,11 +138,23 @@ type Config struct { var config *Config +// startupErrors collects configuration failures raised while packages +// initialise. They cannot be logged where they happen, because logging is not +// configured until the command runs, and printing them bypasses the +// configured log format. Validate reports them instead. +var startupErrors []error + +// RecordStartupError stores a configuration failure for Validate to report. +// Only safe to call during package initialisation, which is single threaded. +func RecordStartupError(err error) { + if err != nil { + startupErrors = append(startupErrors, err) + } +} + func init() { config = &Config{} - if err := env.Parse(config); err != nil { - fmt.Printf("%+v\n", err) - } + RecordStartupError(env.Parse(config)) } func ReadConfig() *Config { @@ -150,6 +163,10 @@ func ReadConfig() *Config { // Validate reports settings that would otherwise fail quietly at request time. func (c *Config) Validate() error { + if len(startupErrors) > 0 { + return fmt.Errorf("invalid configuration: %w", errors.Join(startupErrors...)) + } + if c.OutputFormat.Default == "" { return nil } diff --git a/internal/core/config_test.go b/internal/core/config_test.go new file mode 100644 index 0000000..2c768de --- /dev/null +++ b/internal/core/config_test.go @@ -0,0 +1,56 @@ +package core + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// A malformed setting used to be printed and ignored, so the service started +// with silent defaults. Validate reports it instead. +func TestValidateReportsStartupErrors(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + + startupErrors = []error{errors.New(`strconv.ParseInt: parsing "abc"`)} + + config := *ReadConfig() + err := config.Validate() + + require.Error(t, err) + require.Contains(t, err.Error(), "invalid configuration") + require.Contains(t, err.Error(), "abc") +} + +func TestValidateAcceptsAWorkingConfiguration(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + startupErrors = nil + + config := *ReadConfig() + config.OutputFormat.Default = "" + + require.NoError(t, config.Validate()) +} + +func TestValidateRejectsAnUnknownDefaultFormat(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + startupErrors = nil + + config := *ReadConfig() + config.OutputFormat.Default = "WEBP" + + require.Error(t, config.Validate(), "the map is keyed in lower case") +} + +func TestRecordStartupErrorIgnoresNil(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + startupErrors = nil + + RecordStartupError(nil) + + require.Empty(t, startupErrors) +} diff --git a/internal/core/encryption.go b/internal/core/encryption.go index bcda20d..6b4cc76 100644 --- a/internal/core/encryption.go +++ b/internal/core/encryption.go @@ -13,7 +13,6 @@ import ( "fmt" "github.com/caarlos0/env/v10" "io" - "log/slog" "strings" ) @@ -22,13 +21,11 @@ var salt = []byte("go-dims") func init() { envConfig := Signing{} - if err := env.Parse(&envConfig); err != nil { - fmt.Printf("%+v\n", err) - } + RecordStartupError(env.Parse(&envConfig)) var err error if encryptionKey, err = deriveKey(envConfig.SigningKey); err != nil { - slog.Error("failed to derive encryption key", "error", err) + RecordStartupError(fmt.Errorf("failed to derive the encryption key: %w", err)) return } } diff --git a/internal/core/image.go b/internal/core/image.go index efff7ab..a3dd2b3 100644 --- a/internal/core/image.go +++ b/internal/core/image.go @@ -1,7 +1,6 @@ package core import ( - "fmt" "github.com/beetlebugorg/go-dims/internal/gox/imagex/colorx" "github.com/caarlos0/env/v10" "golang.org/x/exp/slices" @@ -46,9 +45,7 @@ var defaultSourceBackend SourceBackend func RegisterImageBackend(sourceBackend SourceBackend) { config := Source{} - if err := env.Parse(&config); err != nil { - fmt.Printf("%+v\n", err) - } + RecordStartupError(env.Parse(&config)) allSourceBackends = append(allSourceBackends, sourceBackend) diff --git a/internal/source/file.go b/internal/source/file.go index 25edcbd..a52dcef 100644 --- a/internal/source/file.go +++ b/internal/source/file.go @@ -2,7 +2,6 @@ package source import ( "context" - "fmt" "github.com/beetlebugorg/go-dims/internal/core" "github.com/caarlos0/env/v10" "github.com/davidbyttow/govips/v2/vips" @@ -19,9 +18,7 @@ type fileSourceBackend struct { func init() { envConfig := core.FileSource{} - if err := env.Parse(&envConfig); err != nil { - fmt.Printf("%+v\n", err) - } + core.RecordStartupError(env.Parse(&envConfig)) core.RegisterImageBackend(NewFileSourceBackend(envConfig.BaseDir)) } diff --git a/internal/source/s3.go b/internal/source/s3.go index 8fb30aa..d3ff7a3 100644 --- a/internal/source/s3.go +++ b/internal/source/s3.go @@ -2,7 +2,6 @@ package source import ( "context" - "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -37,9 +36,7 @@ var s3Client = sync.OnceValues(func() (*s3.Client, error) { func init() { envConfig := core.S3{} - if err := env.Parse(&envConfig); err != nil { - fmt.Printf("%+v\n", err) - } + core.RecordStartupError(env.Parse(&envConfig)) core.RegisterImageBackend(NewS3SourceBackend(envConfig)) }