diff --git a/backend/modules/socai/handler/chat.go b/backend/modules/socai/handler/chat.go index 82a3f756d..f9ceca2d5 100644 --- a/backend/modules/socai/handler/chat.go +++ b/backend/modules/socai/handler/chat.go @@ -3,13 +3,54 @@ package handler import ( "context" "encoding/json" + "errors" "io" + "net" "net/http" "github.com/gin-gonic/gin" "github.com/threatwinds/go-sdk/catcher" ) +// classifyClientErr maps transport-level failures to a user-safe (httpStatus, message). +// Never echoes err.Error() — it can carry URLs, header values, or plugin internals. +func classifyClientErr(err error) (int, string) { + switch { + case errors.Is(err, context.DeadlineExceeded): + return http.StatusGatewayTimeout, "SOC AI agent timed out" + case errors.Is(err, context.Canceled): + return 499, "request canceled" + } + var ne net.Error + if errors.As(err, &ne) && ne.Timeout() { + return http.StatusGatewayTimeout, "SOC AI agent timed out" + } + return http.StatusBadGateway, "SOC AI agent unreachable" +} + +// messageForStatus maps an upstream HTTP status to a fixed user-safe message, +// so upstream error bodies (which may contain the system prompt) never reach the client. +func messageForStatus(status int) string { + switch status { + case http.StatusUnauthorized, http.StatusForbidden: + return "SOC AI agent rejected credentials" + case http.StatusRequestTimeout, http.StatusGatewayTimeout: + return "SOC AI agent timed out" + case http.StatusTooManyRequests: + return "SOC AI agent rate limited" + case http.StatusNotFound: + return "SOC AI endpoint not found" + case http.StatusBadGateway, http.StatusServiceUnavailable: + return "SOC AI agent unreachable" + case http.StatusBadRequest, http.StatusUnprocessableEntity: + return "SOC AI agent rejected the request" + } + if status >= 500 { + return "SOC AI agent internal error" + } + return "SOC AI agent error" +} + type socAIStreamer interface { StreamAgentTask(ctx context.Context, body []byte) (*http.Response, error) } @@ -65,14 +106,21 @@ func (h *ChatHandler) Chat(c *gin.Context) { resp, err := h.client.StreamAgentTask(c.Request.Context(), body) if err != nil { _ = catcher.Error("SocAIChat: stream request failed", err, nil) - c.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": "SOC AI agent unreachable"}) + status, msg := classifyClientErr(err) + c.JSON(status, gin.H{"status": "error", "message": msg}) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - msg, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) - c.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": string(msg)}) + // Upstream error body may include the loaded plugin config (system prompt, + // masked-but-still-sensitive fields). Log for ops, return a status-derived message. + upstream, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + _ = catcher.Error("SocAIChat: upstream error", nil, map[string]any{ + "status": resp.StatusCode, + "body": string(upstream), + }) + c.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": messageForStatus(resp.StatusCode)}) return } diff --git a/backend/modules/socai/handler/socai.go b/backend/modules/socai/handler/socai.go index c91fa355b..99965effc 100644 --- a/backend/modules/socai/handler/socai.go +++ b/backend/modules/socai/handler/socai.go @@ -61,12 +61,15 @@ func (h *SocAIHandler) Analyze(c *gin.Context) { statusCode, _, err := h.client.Analyze(c.Request.Context(), bodyBytes) if err != nil { - writeError(c, fmt.Sprintf("%s: %s", ctx, err.Error())) + _ = catcher.Error(ctx+": analyze request failed", err, nil) + status, msg := classifyClientErr(err) + c.JSON(status, gin.H{"status": "error", "message": msg}) return } if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { - writeError(c, fmt.Sprintf("%s: unexpected response from SOC AI service: %d", ctx, statusCode)) + _ = catcher.Error(ctx+": unexpected upstream status", nil, map[string]any{"status": statusCode}) + c.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": messageForStatus(statusCode)}) return }