package httpapi import ( "net/http" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) func publicGatewayTask(task store.GatewayTask) store.GatewayTask { if task.Status == "failed" || task.Status == "cancelled" || task.ErrorCode != "" || task.ErrorMessage != "" { value := publicerror.FromFields(task.ErrorCode, firstNonEmpty(task.ErrorMessage, task.Error), taskErrorHTTPStatus(task), false) value = mergeStoredPublicError(task.PublicError, value) value = publicerror.WithIDs(value, task.RequestID, task.ID) publicerror.Observe(value) task.PublicError = &value task.ErrorCode = value.Code task.ErrorMessage = value.Message task.Error = value.Message } task.Attempts = append([]store.TaskAttempt(nil), task.Attempts...) for index := range task.Attempts { attempt := &task.Attempts[index] // User-facing task details expose the canonical task result. Attempt // snapshots can duplicate large input media or retain raw provider output, // so keep them available only on dedicated administrative surfaces. attempt.RequestSnapshot = nil attempt.ResponseSnapshot = nil if attempt.ErrorCode == "" && attempt.ErrorMessage == "" { continue } status := attempt.StatusCode if status <= 0 { status = http.StatusBadGateway } value := publicerror.FromFields(attempt.ErrorCode, attempt.ErrorMessage, status, attempt.Retryable) value = mergeStoredPublicError(attempt.PublicError, value) value = publicerror.WithIDs(value, attempt.RequestID, task.ID) attempt.PublicError = &value attempt.ErrorCode = value.Code attempt.ErrorMessage = value.Message } return task } func publicTaskError(task store.GatewayTask) publicerror.Error { status := taskErrorHTTPStatus(task) if status <= 0 { status = http.StatusBadGateway } value := publicerror.FromFields(task.ErrorCode, firstNonEmpty(task.ErrorMessage, task.Error, task.Message), status, false) value = mergeStoredPublicError(task.PublicError, value) return publicerror.WithIDs(value, task.RequestID, task.ID) } func mergeStoredPublicError(stored *publicerror.Error, derived publicerror.Error) publicerror.Error { if stored == nil || stored.Code == "" || stored.Source == "" { return derived } value := *stored if len(value.Details) == 0 && len(derived.Details) > 0 { value.Details = derived.Details } if message := safeDerivedUpstreamMessage(derived); message != "" { value.Message = message } return value } func safeDerivedUpstreamMessage(value publicerror.Error) string { upstream, _ := value.Details["upstreamError"].(map[string]any) message, _ := upstream["message"].(string) return strings.TrimSpace(message) } func taskErrorHTTPStatus(task store.GatewayTask) int { if task.PublicError != nil && task.PublicError.Source != "" && task.PublicError.HTTPStatus >= 400 { return task.PublicError.HTTPStatus } for index := len(task.Attempts) - 1; index >= 0; index-- { if status := task.Attempts[index].StatusCode; status >= 400 && status <= 599 { return status } } if status := publicErrorInt(task.Metrics["statusCode"]); status >= 400 && status <= 599 { return status } return storedTaskErrorStatus(task.ErrorCode) } func publicErrorInt(value any) int { switch typed := value.(type) { case int: return typed case int32: return int(typed) case int64: return int(typed) case float32: return int(typed) case float64: return int(typed) default: return 0 } } func publicTaskList(items []store.GatewayTask) []store.GatewayTask { out := make([]store.GatewayTask, len(items)) for index, task := range items { out[index] = publicGatewayTask(task) } return out } func publicErrorMap(value publicerror.Error) map[string]any { out := map[string]any{ "code": value.Code, "message": value.Message, "category": value.Category, "source": value.Source, "httpStatus": value.HTTPStatus, "retryable": value.Retryable, "action": value.Action, "version": value.Version, } if value.RetryAfterSeconds > 0 { out["retryAfterSeconds"] = value.RetryAfterSeconds } if requestID := strings.TrimSpace(value.RequestID); requestID != "" { out["requestId"] = requestID } if taskID := strings.TrimSpace(value.TaskID); taskID != "" { out["taskId"] = taskID } if len(value.Details) > 0 { out["details"] = value.Details } return out } func publicErrorWithRetryAfter(value publicerror.Error, details map[string]any) publicerror.Error { if value.RetryAfterSeconds > 0 || details == nil { return value } switch typed := details["retryAfterSeconds"].(type) { case int: value.RetryAfterSeconds = typed case int32: value.RetryAfterSeconds = int(typed) case int64: value.RetryAfterSeconds = int(typed) case float64: value.RetryAfterSeconds = int(typed) } if value.RetryAfterSeconds < 0 { value.RetryAfterSeconds = 0 } return value } func safePublicErrorDetails(details map[string]any, value publicerror.Error, includePublicError bool) map[string]any { out := map[string]any{} for _, key := range []string{"param", "retryAfterSeconds", "recoveryAt", "rateLimit", "pricing"} { if item, ok := details[key]; ok && item != nil { out[key] = item } } if value.Category == "request" { for _, key := range []string{"reason", "diagnosticId"} { if item, ok := details[key]; ok && item != nil { out[key] = item } } } for key, item := range value.Details { if item != nil { out[key] = item } } if includePublicError { out["publicError"] = value } return out }