fix: sanitize API error messages to prevent sensitive info exposure

This commit is contained in:
tinkle-community
2026-01-03 13:11:15 +08:00
parent e07dc0de86
commit 7f7c4ea2a7
5 changed files with 221 additions and 165 deletions
+27 -28
View File
@@ -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
}
+9 -7
View File
@@ -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,
})
}
+95
View File
@@ -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()
}
+73 -115
View File
@@ -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
}
+17 -15
View File
@@ -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