From adfc5d4abeb4c3aca01289b72dc846756a843803 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 11:02:21 +0000 Subject: [PATCH] Abort startup on a config file pixa cannot read (closes #176) Of the places pixa looks for its config file on its own, it passed over any place where os.Stat failed, so a file in a directory pixa may not enter was skipped without a word and pixa started on a later file or on the environment and defaults. Now only a file that does not exist is passed over; any other error aborts startup naming the file, as a file that does not parse already did. README.md says so where it gives the search order. Model: opus-5-5 --- README.md | 8 ++++---- TODO.md | 5 +++++ internal/config/config.go | 29 ++++++++++++++++++----------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 54de083..6e3e2d1 100644 --- a/README.md +++ b/README.md @@ -413,10 +413,10 @@ pixa finds: `/etc/pixa/config.yml`, `/etc/pixa/config.yaml`, `~/.config/pixa/config.yml`, `~/.config/pixa/config.yaml`, then `config.yml` and `config.yaml` in the working directory. A named file that does not exist, cannot be read or does not parse aborts startup. Of the files pixa looks for on -its own, one it finds but cannot read or parse aborts startup; one it cannot -find, for any reason, is passed over without a message, even when the file is -there in a directory pixa may not enter. With no file, pixa uses the environment -and the defaults. +its own, only one that does not exist is passed over, without a message. One +that pixa cannot read or parse aborts startup, naming the file. So does one in a +directory pixa may not enter, whether or not it is there, since pixa cannot +tell. With no file, pixa uses the environment and the defaults. | Variable | Config key | Meaning | | ------------------------------------ | ------------------------------- | ---------------------------------------------------------------------------- | diff --git a/TODO.md b/TODO.md index 1fa426a..b8d42d9 100644 --- a/TODO.md +++ b/TODO.md @@ -29,6 +29,11 @@ P2: security: referer blacklist # Completed Steps +- 2026-10-04 a config file pixa cannot read aborts startup (closes #176): of the + places pixa looks for its config file on its own, only one where the file does + not exist is passed over; any other error, such as a directory on the path + that pixa may not enter, aborts startup naming the file, as a file that does + not parse already did. - 2026-10-04 deployment guide and example Caddy config (closes #89): "Deployment" in `README.md` says what the reverse proxy in front of pixa must do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set diff --git a/internal/config/config.go b/internal/config/config.go index b8e041b..35254b3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ package config import ( "errors" "fmt" + "io/fs" "log/slog" "math" "net/netip" @@ -778,19 +779,25 @@ func loadConfigFile(log *slog.Logger, appName string) (*smartconfig.Config, erro for _, path := range configPaths { cleanPath := filepath.Clean(path) + // Only a config file that does not exist is skipped. One that + // cannot be read or does not parse is a fatal startup error. _, statErr := os.Stat(cleanPath) - if statErr == nil { - // A config file that exists but does not parse is a fatal - // startup error, never something to skip over. - sc, err := smartconfig.NewFromConfigPath(path) - if err != nil { - return nil, fmt.Errorf("failed to parse config file %s: %w", path, err) - } - - log.Info("loaded config file", "path", path) - - return sc, nil + if errors.Is(statErr, fs.ErrNotExist) { + continue } + + if statErr != nil { + return nil, fmt.Errorf("failed to read config file %s: %w", path, statErr) + } + + sc, err := smartconfig.NewFromConfigPath(path) + if err != nil { + return nil, fmt.Errorf("failed to parse config file %s: %w", path, err) + } + + log.Info("loaded config file", "path", path) + + return sc, nil } return nil, nil //nolint:nilnil // nil config is valid (use defaults)