diff --git a/api/backtest.go b/api/backtest.go index 8e0014b9..9d0e2be3 100644 --- a/api/backtest.go +++ b/api/backtest.go @@ -60,7 +60,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) { var req backtestStartRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -78,16 +78,16 @@ func (s *Server) handleBacktestStart(c *gin.Context) { if cfg.StrategyID != "" { strategy, err := s.store.Strategy().Get(cfg.UserID, cfg.StrategyID) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to load strategy: %v", err)}) + SafeBadRequest(c, "Failed to load strategy") return } if strategy == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("strategy not found: %s", cfg.StrategyID)}) + SafeBadRequest(c, "Strategy not found") return } var strategyConfig store.StrategyConfig if err := json.Unmarshal([]byte(strategy.Config), &strategyConfig); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse strategy config: %v", err)}) + SafeBadRequest(c, "Failed to parse strategy config") return } cfg.SetLoadedStrategy(&strategyConfig) @@ -102,7 +102,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) { if len(cfg.Symbols) == 0 { symbols, err := s.resolveStrategyCoins(&strategyConfig) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to resolve coins from strategy: %v", err)}) + SafeBadRequest(c, "Failed to resolve coins from strategy") return } cfg.Symbols = symbols @@ -111,7 +111,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) { } if err := s.hydrateBacktestAIConfig(&cfg); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Failed to configure AI model") return } @@ -120,7 +120,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) { runner, err := s.backtestManager.Start(context.Background(), cfg) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeError(c, http.StatusBadRequest, "Failed to start backtest", err) return } @@ -149,11 +149,11 @@ func (s *Server) handleBacktestControl(c *gin.Context, fn func(string) error) { var req runIDRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } if req.RunID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"}) + SafeBadRequest(c, "run_id is required") return } @@ -162,7 +162,7 @@ func (s *Server) handleBacktestControl(c *gin.Context, fn func(string) error) { } if err := fn(req.RunID); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeError(c, http.StatusBadRequest, "Failed to execute backtest operation", err) return } @@ -181,11 +181,11 @@ func (s *Server) handleBacktestLabel(c *gin.Context) { } var req labelRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } if strings.TrimSpace(req.RunID) == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"}) + SafeBadRequest(c, "run_id is required") return } userID := normalizeUserID(c.GetString("user_id")) @@ -194,7 +194,7 @@ func (s *Server) handleBacktestLabel(c *gin.Context) { } meta, err := s.backtestManager.UpdateLabel(req.RunID, req.Label) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Update backtest label", err) return } c.JSON(http.StatusOK, meta) @@ -207,11 +207,11 @@ func (s *Server) handleBacktestDelete(c *gin.Context) { } var req runIDRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } if strings.TrimSpace(req.RunID) == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "run_id is required"}) + SafeBadRequest(c, "run_id is required") return } userID := normalizeUserID(c.GetString("user_id")) @@ -219,7 +219,7 @@ func (s *Server) handleBacktestDelete(c *gin.Context) { return } if err := s.backtestManager.Delete(req.RunID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Delete backtest run", err) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) @@ -277,7 +277,7 @@ func (s *Server) handleBacktestRuns(c *gin.Context) { metas, err := s.backtestManager.ListRuns() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "List backtest runs", err) return } stateFilter := strings.ToLower(strings.TrimSpace(c.Query("state"))) @@ -349,7 +349,7 @@ func (s *Server) handleBacktestEquity(c *gin.Context) { points, err := s.backtestManager.LoadEquity(runID, timeframe, limit) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeError(c, http.StatusBadRequest, "Failed to load equity data", err) return } c.JSON(http.StatusOK, points) @@ -375,7 +375,7 @@ func (s *Server) handleBacktestTrades(c *gin.Context) { events, err := s.backtestManager.LoadTrades(runID, limit) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeError(c, http.StatusBadRequest, "Failed to load trades", err) return } c.JSON(http.StatusOK, events) @@ -404,7 +404,7 @@ func (s *Server) handleBacktestMetrics(c *gin.Context) { c.JSON(http.StatusAccepted, gin.H{"error": "metrics not ready yet"}) return } - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeError(c, http.StatusBadRequest, "Failed to load metrics", err) return } c.JSON(http.StatusOK, metrics) @@ -427,7 +427,7 @@ func (s *Server) handleBacktestTrace(c *gin.Context) { cycle := queryInt(c, "cycle", 0) record, err := s.backtestManager.GetTrace(runID, cycle) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trace record") return } c.JSON(http.StatusOK, record) @@ -461,7 +461,7 @@ func (s *Server) handleBacktestDecisions(c *gin.Context) { records, err := backtest.LoadDecisionRecords(runID, limit, offset) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Load decision records", err) return } c.JSON(http.StatusOK, records) @@ -483,7 +483,7 @@ func (s *Server) handleBacktestExport(c *gin.Context) { } path, err := s.backtestManager.ExportRun(runID) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeError(c, http.StatusBadRequest, "Failed to export backtest", err) return } defer os.Remove(path) @@ -536,8 +536,7 @@ func (s *Server) handleBacktestKlines(c *gin.Context) { klines, err := market.GetKlinesRange(symbol, timeframe, startTime, endTime) if err != nil { - logger.Errorf("Failed to fetch klines for %s: %v", symbol, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to fetch klines: %v", err)}) + SafeInternalError(c, "Fetch klines", err) return } @@ -620,11 +619,11 @@ func writeBacktestAccessError(c *gin.Context, err error) bool { } switch { case errors.Is(err, errBacktestForbidden): - c.JSON(http.StatusForbidden, gin.H{"error": "No permission to access this backtest task"}) + SafeForbidden(c, "No permission to access this backtest task") case errors.Is(err, os.ErrNotExist), errors.Is(err, sql.ErrNoRows): - c.JSON(http.StatusNotFound, gin.H{"error": "Backtest task does not exist"}) + SafeNotFound(c, "Backtest task") default: - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Access backtest", err) } return true } diff --git a/api/debate.go b/api/debate.go index e2190fde..5f5f52bd 100644 --- a/api/debate.go +++ b/api/debate.go @@ -131,7 +131,7 @@ func (h *DebateHandler) HandleCreateDebate(c *gin.Context) { var req CreateDebateRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -292,7 +292,7 @@ func (h *DebateHandler) HandleStartDebate(c *gin.Context) { // Start debate asynchronously if err := h.engine.StartDebate(debateID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Start debate", err) return } @@ -316,7 +316,7 @@ func (h *DebateHandler) HandleCancelDebate(c *gin.Context) { } if err := h.engine.CancelDebate(debateID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Cancel debate", err) return } @@ -495,20 +495,20 @@ func (h *DebateHandler) HandleExecuteDebate(c *gin.Context) { // Parse request var req ExecuteDebateRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Get trader executor executor, err := h.traderManager.GetTraderExecutor(req.TraderID) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("trader not available: %v", err)}) + SafeError(c, http.StatusBadRequest, "Trader not available", err) return } // Execute consensus if err := h.engine.ExecuteConsensus(debateID, executor); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + SafeInternalError(c, "Execute consensus", err) return } @@ -635,7 +635,9 @@ func (h *DebateHandler) broadcastConsensus(sessionID string, decision *store.Deb } func (h *DebateHandler) broadcastError(sessionID string, err error) { + // Sanitize error message before broadcasting to client + safeMsg := SanitizeError(err, "An error occurred during debate") h.broadcast(sessionID, "error", map[string]interface{}{ - "error": err.Error(), + "error": safeMsg, }) } diff --git a/api/errors.go b/api/errors.go new file mode 100644 index 00000000..13e51fcb --- /dev/null +++ b/api/errors.go @@ -0,0 +1,95 @@ +package api + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "nofx/logger" +) + +// SafeError returns a safe error message without exposing internal details +// It logs the actual error for debugging but returns a generic message to the client +func SafeError(c *gin.Context, statusCode int, publicMsg string, internalErr error) { + // Log the actual error internally + if internalErr != nil { + logger.Errorf("[API Error] %s: %v", publicMsg, internalErr) + } + + c.JSON(statusCode, gin.H{"error": publicMsg}) +} + +// SafeInternalError logs internal error and returns a generic message +func SafeInternalError(c *gin.Context, operation string, err error) { + logger.Errorf("[Internal Error] %s: %v", operation, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": operation + " failed"}) +} + +// SafeBadRequest returns a safe bad request error +// For validation errors, we can be more specific since they're about user input +func SafeBadRequest(c *gin.Context, msg string) { + c.JSON(http.StatusBadRequest, gin.H{"error": msg}) +} + +// SafeNotFound returns a generic not found error +func SafeNotFound(c *gin.Context, resource string) { + c.JSON(http.StatusNotFound, gin.H{"error": resource + " not found"}) +} + +// SafeUnauthorized returns unauthorized error +func SafeUnauthorized(c *gin.Context) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) +} + +// SafeForbidden returns forbidden error +func SafeForbidden(c *gin.Context, msg string) { + c.JSON(http.StatusForbidden, gin.H{"error": msg}) +} + +// IsSensitiveError checks if an error message contains sensitive information +func IsSensitiveError(err error) bool { + if err == nil { + return false + } + errMsg := strings.ToLower(err.Error()) + + sensitivePatterns := []string{ + // Database + "postgres", "mysql", "sqlite", "database", "sql", + "connection", "connect", "failed to connect", + // Network + "dial", "tcp", "udp", "socket", "timeout", + // Server info + "127.0.0.1", "localhost", "0.0.0.0", + // File system + "no such file", "permission denied", "open /", + // Credentials + "password", "user=", "host=", "port=", + // Internal + "panic", "runtime error", "stack trace", + } + + for _, pattern := range sensitivePatterns { + if strings.Contains(errMsg, pattern) { + return true + } + } + + // Check for IP addresses (simple pattern) + if strings.Contains(errMsg, ":") && (strings.Contains(errMsg, ".") || strings.Contains(errMsg, "::")) { + return true + } + + return false +} + +// SanitizeError returns the error message if safe, otherwise returns a generic message +func SanitizeError(err error, fallbackMsg string) string { + if err == nil { + return fallbackMsg + } + if IsSensitiveError(err) { + return fallbackMsg + } + return err.Error() +} diff --git a/api/server.go b/api/server.go index 1d519ded..7f16ec26 100644 --- a/api/server.go +++ b/api/server.go @@ -486,7 +486,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { userID := c.GetString("user_id") var req CreateTraderRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -682,7 +682,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { err = s.store.Trader().Create(traderRecord) if err != nil { logger.Infof("❌ Failed to create trader: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create trader: %v", err)}) + SafeInternalError(c, "Failed to create trader", err) return } logger.Infof("🔧 DEBUG: CreateTrader succeeded") @@ -732,7 +732,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { var req UpdateTraderRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -823,7 +823,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { traderRecord.ID, traderRecord.Name, traderRecord.AIModelID, traderRecord.StrategyID, req.StrategyID) err = s.store.Trader().Update(traderRecord) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update trader: %v", err)}) + SafeInternalError(c, "Failed to update trader", err) return } @@ -854,7 +854,7 @@ func (s *Server) handleDeleteTrader(c *gin.Context) { // Delete from database err := s.store.Trader().Delete(userID, traderID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete trader: %v", err)}) + SafeInternalError(c, "Failed to delete trader", err) return } @@ -1012,14 +1012,14 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Update database err := s.store.Trader().UpdateCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update custom prompt: %v", err)}) + SafeInternalError(c, "Failed to update custom prompt", err) return } @@ -1044,14 +1044,14 @@ func (s *Server) handleToggleCompetition(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Update database err := s.store.Trader().UpdateShowInCompetition(userID, traderID, req.ShowInCompetition) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update competition visibility: %v", err)}) + SafeInternalError(c, "Update competition visibility", err) return } @@ -1150,7 +1150,7 @@ func (s *Server) handleSyncBalance(c *gin.Context) { if createErr != nil { logger.Infof("⚠️ Failed to create temporary trader: %v", createErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to connect to exchange: %v", createErr)}) + SafeInternalError(c, "Failed to connect to exchange", createErr) return } @@ -1158,7 +1158,7 @@ func (s *Server) handleSyncBalance(c *gin.Context) { balanceInfo, balanceErr := tempTrader.GetBalance() if balanceErr != nil { logger.Infof("⚠️ Failed to query exchange balance: %v", balanceErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to query balance: %v", balanceErr)}) + SafeInternalError(c, "Failed to query balance", balanceErr) return } @@ -1302,7 +1302,7 @@ func (s *Server) handleClosePosition(c *gin.Context) { if createErr != nil { logger.Infof("⚠️ Failed to create temporary trader: %v", createErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to connect to exchange: %v", createErr)}) + SafeInternalError(c, "Failed to connect to exchange", createErr) return } @@ -1344,7 +1344,7 @@ func (s *Server) handleClosePosition(c *gin.Context) { if closeErr != nil { logger.Infof("❌ Close position failed: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to close position: %v", closeErr)}) + SafeInternalError(c, "Failed to close position", closeErr) return } @@ -1582,7 +1582,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) { models, err := s.store.AIModel().List(userID) if err != nil { logger.Infof("❌ Failed to get AI model configs: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get AI model configs: %v", err)}) + SafeInternalError(c, "Failed to get AI model configs", err) return } @@ -1684,7 +1684,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { for modelID, modelData := range req.Models { err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update model %s: %v", modelID, err)}) + SafeInternalError(c, fmt.Sprintf("Update model %s", modelID), err) return } } @@ -1706,8 +1706,7 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) { logger.Infof("🔍 Querying exchange configs for user %s", userID) exchanges, err := s.store.Exchange().List(userID) if err != nil { - logger.Infof("❌ Failed to get exchange configs: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get exchange configs: %v", err)}) + SafeInternalError(c, "Failed to get exchange configs", err) return } @@ -1805,7 +1804,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { for exchangeID, exchangeData := range req.Exchanges { err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update exchange %s: %v", exchangeID, err)}) + SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err) return } } @@ -1910,7 +1909,7 @@ func (s *Server) handleCreateExchange(c *gin.Context) { ) if err != nil { logger.Infof("❌ Failed to create exchange account: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create exchange account: %v", err)}) + SafeInternalError(c, "Failed to create exchange account", err) return } @@ -1953,7 +1952,7 @@ func (s *Server) handleDeleteExchange(c *gin.Context) { err = s.store.Exchange().Delete(userID, exchangeID) if err != nil { logger.Infof("❌ Failed to delete exchange account: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete exchange account: %v", err)}) + SafeInternalError(c, "Failed to delete exchange account", err) return } @@ -1966,7 +1965,7 @@ func (s *Server) handleTraderList(c *gin.Context) { userID := c.GetString("user_id") traders, err := s.store.Trader().List(userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get trader list: %v", err)}) + SafeInternalError(c, "Failed to get trader list", err) return } @@ -2019,7 +2018,7 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { fullCfg, err := s.store.Trader().GetFullConfig(userID, traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Failed to get trader config: %v", err)}) + SafeNotFound(c, "Trader config") return } traderConfig := fullCfg.Trader @@ -2062,13 +2061,13 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { func (s *Server) handleStatus(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } @@ -2080,23 +2079,20 @@ func (s *Server) handleStatus(c *gin.Context) { func (s *Server) handleAccount(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } logger.Infof("📊 Received account info request [%s]", trader.GetName()) account, err := trader.GetAccountInfo() if err != nil { - logger.Infof("❌ Failed to get account info [%s]: %v", trader.GetName(), err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get account info: %v", err), - }) + SafeInternalError(c, "Get account info", err) return } @@ -2113,21 +2109,19 @@ func (s *Server) handleAccount(c *gin.Context) { func (s *Server) handlePositions(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } positions, err := trader.GetPositions() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get position list: %v", err), - }) + SafeInternalError(c, "Get positions", err) return } @@ -2138,13 +2132,13 @@ func (s *Server) handlePositions(c *gin.Context) { func (s *Server) handlePositionHistory(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } @@ -2165,9 +2159,7 @@ func (s *Server) handlePositionHistory(c *gin.Context) { // Get closed positions positions, err := store.Position().GetClosedPositions(trader.GetID(), limit) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get position history: %v", err), - }) + SafeInternalError(c, "Get position history", err) return } @@ -2192,13 +2184,13 @@ func (s *Server) handlePositionHistory(c *gin.Context) { func (s *Server) handleTrades(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } @@ -2224,9 +2216,7 @@ func (s *Server) handleTrades(c *gin.Context) { allTrades, err := store.Position().GetRecentTrades(trader.GetID(), limit) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get trades: %v", err), - }) + SafeInternalError(c, "Get trades", err) return } @@ -2249,13 +2239,13 @@ func (s *Server) handleTrades(c *gin.Context) { func (s *Server) handleOrders(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } @@ -2283,9 +2273,7 @@ func (s *Server) handleOrders(c *gin.Context) { // Get all orders for this trader allOrders, err := store.Order().GetTraderOrders(trader.GetID(), limit) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get orders: %v", err), - }) + SafeInternalError(c, "Get orders", err) return } @@ -2317,13 +2305,13 @@ func (s *Server) handleOrderFills(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } @@ -2336,9 +2324,7 @@ func (s *Server) handleOrderFills(c *gin.Context) { // Get fills for this order fills, err := store.Order().GetOrderFills(orderID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get order fills: %v", err), - }) + SafeInternalError(c, "Get order fills", err) return } @@ -2376,30 +2362,21 @@ func (s *Server) handleKlines(c *gin.Context) { // US Stocks via Alpaca klines, err = s.getKlinesFromAlpaca(symbol, interval, limit) if err != nil { - logger.Errorf("❌ Alpaca API failed for %s: %v", symbol, err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get klines from Alpaca: %v", err), - }) + SafeInternalError(c, "Get klines from Alpaca", err) return } case "forex", "metals": // Forex and Metals via Twelve Data klines, err = s.getKlinesFromTwelveData(symbol, interval, limit) if err != nil { - logger.Errorf("❌ TwelveData API failed for %s: %v", symbol, err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get klines from TwelveData: %v", err), - }) + SafeInternalError(c, "Get klines from TwelveData", err) return } case "hyperliquid", "hyperliquid-xyz", "xyz": // Hyperliquid native API - supports both crypto perps and stock perps (xyz dex) klines, err = s.getKlinesFromHyperliquid(symbol, interval, limit) if err != nil { - logger.Errorf("❌ Hyperliquid API failed for %s: %v", symbol, err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get klines from Hyperliquid: %v", err), - }) + SafeInternalError(c, "Get klines from Hyperliquid", err) return } default: @@ -2407,10 +2384,7 @@ func (s *Server) handleKlines(c *gin.Context) { symbol = market.Normalize(symbol) klines, err = s.getKlinesFromCoinank(symbol, interval, exchange, limit) if err != nil { - logger.Errorf("❌ CoinAnk API failed for %s on %s: %v", symbol, exchange, err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get klines from CoinAnk: %v", err), - }) + SafeInternalError(c, "Get klines from CoinAnk", err) return } } @@ -2728,22 +2702,20 @@ func (s *Server) handleSymbols(c *gin.Context) { func (s *Server) handleDecisions(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } // Get all historical decision records (unlimited) records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 10000) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get decision log: %v", err), - }) + SafeInternalError(c, "Get decision log", err) return } @@ -2754,13 +2726,13 @@ func (s *Server) handleDecisions(c *gin.Context) { func (s *Server) handleLatestDecisions(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } @@ -2777,9 +2749,7 @@ func (s *Server) handleLatestDecisions(c *gin.Context) { records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), limit) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get decision log: %v", err), - }) + SafeInternalError(c, "Get decision log", err) return } @@ -2796,21 +2766,19 @@ func (s *Server) handleLatestDecisions(c *gin.Context) { func (s *Server) handleStatistics(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } trader, err := s.traderManager.GetTrader(traderID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + SafeNotFound(c, "Trader") return } stats, err := trader.GetStore().Decision().GetStatistics(trader.GetID()) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get statistics: %v", err), - }) + SafeInternalError(c, "Get statistics", err) return } @@ -2829,9 +2797,7 @@ func (s *Server) handleCompetition(c *gin.Context) { competition, err := s.traderManager.GetCompetitionData() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get competition data: %v", err), - }) + SafeInternalError(c, "Get competition data", err) return } @@ -2843,7 +2809,7 @@ func (s *Server) handleCompetition(c *gin.Context) { func (s *Server) handleEquityHistory(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid trader ID") return } @@ -2851,9 +2817,7 @@ func (s *Server) handleEquityHistory(c *gin.Context) { // Every 3 minutes per cycle: 10000 records = about 20 days of data snapshots, err := s.store.Equity().GetLatest(traderID, 10000) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get historical data: %v", err), - }) + SafeInternalError(c, "Get historical data", err) return } @@ -2931,7 +2895,8 @@ func (s *Server) authMiddleware() gin.HandlerFunc { // Validate JWT token claims, err := auth.ValidateJWT(tokenString) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: " + err.Error()}) + logger.Errorf("[Auth] Invalid token: %v", err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) c.Abort() return } @@ -2999,7 +2964,7 @@ func (s *Server) handleRegister(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -3036,7 +3001,7 @@ func (s *Server) handleRegister(c *gin.Context) { err = s.store.User().Create(user) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()}) + SafeInternalError(c, "Failed to create user", err) return } @@ -3059,14 +3024,14 @@ func (s *Server) handleCompleteRegistration(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Get user information user, err := s.store.User().GetByID(req.UserID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "User does not exist"}) + SafeNotFound(c, "User") return } @@ -3112,7 +3077,7 @@ func (s *Server) handleLogin(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -3156,14 +3121,14 @@ func (s *Server) handleVerifyOTP(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Get user information user, err := s.store.User().GetByID(req.UserID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "User does not exist"}) + SafeNotFound(c, "User") return } @@ -3197,7 +3162,7 @@ func (s *Server) handleResetPassword(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -3326,9 +3291,7 @@ func (s *Server) handlePublicTraderList(c *gin.Context) { // Get trader information from all users competition, err := s.traderManager.GetCompetitionData() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get trader list: %v", err), - }) + SafeInternalError(c, "Get trader list", err) return } @@ -3371,9 +3334,7 @@ func (s *Server) handlePublicTraderList(c *gin.Context) { func (s *Server) handlePublicCompetition(c *gin.Context) { competition, err := s.traderManager.GetCompetitionData() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get competition data: %v", err), - }) + SafeInternalError(c, "Get competition data", err) return } @@ -3384,9 +3345,7 @@ func (s *Server) handlePublicCompetition(c *gin.Context) { func (s *Server) handleTopTraders(c *gin.Context) { topTraders, err := s.traderManager.GetTopTradersData() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get top 10 trader data: %v", err), - }) + SafeInternalError(c, "Get top traders data", err) return } @@ -3409,9 +3368,7 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { // If no trader_ids specified, return historical data for top 5 topTraders, err := s.traderManager.GetTopTradersData() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("Failed to get top 5 traders: %v", err), - }) + SafeInternalError(c, "Get top traders", err) return } @@ -3506,7 +3463,8 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string, hours int) map[s snapshots, err = s.store.Equity().GetLatest(traderID, 500) } if err != nil { - errors[traderID] = fmt.Sprintf("Failed to get historical data: %v", err) + logger.Errorf("[API] Failed to get equity history for %s: %v", traderID, err) + errors[traderID] = "Failed to get historical data" continue } diff --git a/api/strategy.go b/api/strategy.go index c33edeb2..cadb82fb 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "nofx/decision" + "nofx/logger" "nofx/market" "nofx/mcp" "nofx/store" @@ -33,7 +34,7 @@ func validateStrategyConfig(config *store.StrategyConfig) []string { func (s *Server) handlePublicStrategies(c *gin.Context) { strategies, err := s.store.Strategy().ListPublic() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get public strategies: " + err.Error()}) + SafeInternalError(c, "Failed to get public strategies", err) return } @@ -76,7 +77,7 @@ func (s *Server) handleGetStrategies(c *gin.Context) { strategies, err := s.store.Strategy().List(userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get strategy list: " + err.Error()}) + SafeInternalError(c, "Failed to get strategy list", err) return } @@ -151,14 +152,14 @@ func (s *Server) handleCreateStrategy(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Serialize configuration configJSON, err := json.Marshal(req.Config) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"}) + SafeInternalError(c, "Serialize configuration", err) return } @@ -173,7 +174,7 @@ func (s *Server) handleCreateStrategy(c *gin.Context) { } if err := s.store.Strategy().Create(strategy); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create strategy: " + err.Error()}) + SafeInternalError(c, "Failed to create strategy", err) return } @@ -221,14 +222,14 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } // Serialize configuration configJSON, err := json.Marshal(req.Config) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize configuration"}) + SafeInternalError(c, "Serialize configuration", err) return } @@ -243,7 +244,7 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) { } if err := s.store.Strategy().Update(strategy); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update strategy: " + err.Error()}) + SafeInternalError(c, "Failed to update strategy", err) return } @@ -269,7 +270,7 @@ func (s *Server) handleDeleteStrategy(c *gin.Context) { } if err := s.store.Strategy().Delete(userID, strategyID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete strategy: " + err.Error()}) + SafeInternalError(c, "Failed to delete strategy", err) return } @@ -287,7 +288,7 @@ func (s *Server) handleActivateStrategy(c *gin.Context) { } if err := s.store.Strategy().SetActive(userID, strategyID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to activate strategy: " + err.Error()}) + SafeInternalError(c, "Failed to activate strategy", err) return } @@ -309,13 +310,13 @@ func (s *Server) handleDuplicateStrategy(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } newID := uuid.New().String() if err := s.store.Strategy().Duplicate(userID, sourceID, newID, req.Name); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to duplicate strategy: " + err.Error()}) + SafeInternalError(c, "Failed to duplicate strategy", err) return } @@ -383,7 +384,7 @@ func (s *Server) handlePreviewPrompt(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -433,7 +434,7 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters: " + err.Error()}) + SafeBadRequest(c, "Invalid request parameters") return } @@ -447,8 +448,9 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) { // Get candidate coins candidates, err := engine.GetCandidateCoins() if err != nil { + logger.Errorf("[API Error] Failed to get candidate coins: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get candidate coins: " + err.Error(), + "error": "Failed to get candidate coins", "ai_response": "", }) return