From 61a9fcedcbaea1bceb66a2ad70cfeb66d60c8579 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Mon, 11 May 2026 15:45:10 -0400 Subject: [PATCH] fix(config): individual errors on json parse --- internal/config/load.go | 6 ++++++ internal/config/load_test.go | 32 ++++++++++++++++++++++++++++++++ internal/config/store.go | 3 +++ 3 files changed, 41 insertions(+) diff --git a/internal/config/load.go b/internal/config/load.go index 367decab920d584de1fadfd377a78e43dd67dfe6..d7a04e27c1e35e91c49a034e09c2e0d926ee536d 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -55,6 +55,9 @@ func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) { // Load workspace config last so it has highest priority. if wsData, err := os.ReadFile(store.workspacePath); err == nil && len(wsData) > 0 { + if !json.Valid(wsData) { + return nil, fmt.Errorf("invalid JSON in config file %s", store.workspacePath) + } merged, mergeErr := loadFromBytes(append([][]byte{mustMarshalConfig(cfg)}, wsData)) if mergeErr == nil { // Preserve defaults that setDefaults already applied. @@ -734,6 +737,9 @@ func loadFromConfigPaths(configPaths []string) (*Config, []string, error) { if len(data) == 0 { continue } + if !json.Valid(data) { + return nil, nil, fmt.Errorf("invalid JSON in config file %s", path) + } configs = append(configs, data) loaded = append(loaded, path) } diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 61054e5ccbc9031702f3d1d778634bf53b625bcb..991012616b33daa6d439d93c7cc266d6e83f2e5c 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -36,6 +36,38 @@ func TestConfig_LoadFromBytes(t *testing.T) { require.Equal(t, "https://api.openai.com/v2", pc.BaseURL) } +func TestLoadFromConfigPaths_InvalidJSON(t *testing.T) { + t.Parallel() + + t.Run("identifies the offending file", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + good := filepath.Join(tmpDir, "good.json") + bad := filepath.Join(tmpDir, "bad.json") + require.NoError(t, os.WriteFile(good, []byte(`{"providers":{}}`), 0o644)) + require.NoError(t, os.WriteFile(bad, []byte(`{not valid json}`), 0o644)) + + _, _, err := loadFromConfigPaths([]string{good, bad}) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid JSON in config file") + require.Contains(t, err.Error(), "bad.json") + }) + + t.Run("skips missing and empty files", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + empty := filepath.Join(tmpDir, "empty.json") + require.NoError(t, os.WriteFile(empty, []byte(""), 0o644)) + + cfg, _, err := loadFromConfigPaths([]string{ + filepath.Join(tmpDir, "nonexistent.json"), + empty, + }) + require.NoError(t, err) + require.NotNil(t, cfg) + }) +} + // testStore wraps a Config in a minimal ConfigStore for testing. func testStore(cfg *Config) *ConfigStore { return &ConfigStore{config: cfg} diff --git a/internal/config/store.go b/internal/config/store.go index 376efb93feada30d4710929d9799a672a436f6e4..5501bdddafd206c4294a22666b59399338db7503 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -647,6 +647,9 @@ func (s *ConfigStore) ReloadFromDisk(ctx context.Context) error { // Merge workspace config if present workspacePath := filepath.Join(cfg.Options.DataDirectory, fmt.Sprintf("%s.json", appName)) if wsData, err := os.ReadFile(workspacePath); err == nil && len(wsData) > 0 { + if !json.Valid(wsData) { + return fmt.Errorf("invalid JSON in config file %s", workspacePath) + } merged, mergeErr := loadFromBytes(append([][]byte{mustMarshalConfig(cfg)}, wsData)) if mergeErr == nil { dataDir := cfg.Options.DataDirectory