From ed9230d38b98cf90e570e3580935f2b846f552ab Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 23 Mar 2026 12:10:55 +0800 Subject: [PATCH] fix: toFloat handles json.Number, CORS credentials+wildcard bug, strategy config error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent/agent.go: toFloat() now handles json.Number and int32 types to prevent silent data loss - api/server.go: CORS fix — echo specific origin instead of '*' when credentials are enabled (browsers reject Allow-Credentials with wildcard) - api/strategy.go: all 4 json.Unmarshal calls now check errors and return 'config_error' field instead of silently serving zero-value configs - store/decision.go: explicitly mark best-effort unmarshal with _ = assignment for clarity --- agent/agent.go | 3 +++ api/server.go | 9 ++++++++- api/strategy.go | 46 ++++++++++++++++++++++++++++++++-------------- store/decision.go | 7 ++++--- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 4e11ccdc..97cea1a0 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -7,6 +7,7 @@ package agent import ( "context" + "encoding/json" "fmt" "log/slog" "net/http" @@ -548,7 +549,9 @@ func toFloat(v interface{}) float64 { case float32: return float64(x) case int: return float64(x) case int64: return float64(x) + case int32: return float64(x) case string: f, _ := strconv.ParseFloat(x, 64); return f + case json.Number: f, _ := x.Float64(); return f } return 0 } diff --git a/api/server.go b/api/server.go index 25a950e1..c79d716f 100644 --- a/api/server.go +++ b/api/server.go @@ -98,7 +98,14 @@ func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { origin := c.Request.Header.Get("Origin") if allowAll { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + if origin != "" { + // When credentials are needed, we must echo the specific origin + // instead of "*" (browsers reject Allow-Credentials with wildcard). + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + c.Writer.Header().Set("Vary", "Origin") + } else { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + } } else if origin != "" && allowed[origin] { c.Writer.Header().Set("Access-Control-Allow-Origin", origin) c.Writer.Header().Set("Vary", "Origin") diff --git a/api/strategy.go b/api/strategy.go index c58f3278..407ed82e 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -56,8 +56,11 @@ func (s *Server) handlePublicStrategies(c *gin.Context) { // Only include config if config_visible is true if st.ConfigVisible { var config store.StrategyConfig - json.Unmarshal([]byte(st.Config), &config) - item["config"] = config + if err := json.Unmarshal([]byte(st.Config), &config); err != nil { + item["config_error"] = "invalid config format" + } else { + item["config"] = config + } } result = append(result, item) @@ -86,9 +89,9 @@ func (s *Server) handleGetStrategies(c *gin.Context) { result := make([]gin.H, 0, len(strategies)) for _, st := range strategies { var config store.StrategyConfig - json.Unmarshal([]byte(st.Config), &config) + configErr := json.Unmarshal([]byte(st.Config), &config) - result = append(result, gin.H{ + item := gin.H{ "id": st.ID, "name": st.Name, "description": st.Description, @@ -96,10 +99,15 @@ func (s *Server) handleGetStrategies(c *gin.Context) { "is_default": st.IsDefault, "is_public": st.IsPublic, "config_visible": st.ConfigVisible, - "config": config, "created_at": st.CreatedAt, "updated_at": st.UpdatedAt, - }) + } + if configErr != nil { + item["config_error"] = "invalid config format" + } else { + item["config"] = config + } + result = append(result, item) } c.JSON(http.StatusOK, gin.H{ @@ -124,18 +132,23 @@ func (s *Server) handleGetStrategy(c *gin.Context) { } var config store.StrategyConfig - json.Unmarshal([]byte(strategy.Config), &config) + configErr := json.Unmarshal([]byte(strategy.Config), &config) - c.JSON(http.StatusOK, gin.H{ + result := gin.H{ "id": strategy.ID, "name": strategy.Name, "description": strategy.Description, "is_active": strategy.IsActive, "is_default": strategy.IsDefault, - "config": config, "created_at": strategy.CreatedAt, "updated_at": strategy.UpdatedAt, - }) + } + if configErr != nil { + result["config_error"] = "invalid config format" + } else { + result["config"] = config + } + c.JSON(http.StatusOK, result) } // handleCreateStrategy Create strategy. @@ -383,18 +396,23 @@ func (s *Server) handleGetActiveStrategy(c *gin.Context) { } var config store.StrategyConfig - json.Unmarshal([]byte(strategy.Config), &config) + configErr := json.Unmarshal([]byte(strategy.Config), &config) - c.JSON(http.StatusOK, gin.H{ + result := gin.H{ "id": strategy.ID, "name": strategy.Name, "description": strategy.Description, "is_active": strategy.IsActive, "is_default": strategy.IsDefault, - "config": config, "created_at": strategy.CreatedAt, "updated_at": strategy.UpdatedAt, - }) + } + if configErr != nil { + result["config_error"] = "invalid config format" + } else { + result["config"] = config + } + c.JSON(http.StatusOK, result) } // handleGetDefaultStrategyConfig Get default strategy configuration template diff --git a/store/decision.go b/store/decision.go index 6926f326..e4c74c67 100644 --- a/store/decision.go +++ b/store/decision.go @@ -138,9 +138,10 @@ func (db *DecisionRecordDB) toRecord() *DecisionRecord { ErrorMessage: db.ErrorMessage, AIRequestDurationMs: db.AIRequestDurationMs, } - json.Unmarshal([]byte(db.CandidateCoins), &record.CandidateCoins) - json.Unmarshal([]byte(db.ExecutionLog), &record.ExecutionLog) - json.Unmarshal([]byte(db.Decisions), &record.Decisions) + // Best-effort unmarshal — empty/malformed JSON fields are left as zero values. + _ = json.Unmarshal([]byte(db.CandidateCoins), &record.CandidateCoins) + _ = json.Unmarshal([]byte(db.ExecutionLog), &record.ExecutionLog) + _ = json.Unmarshal([]byte(db.Decisions), &record.Decisions) return record }