package soraerror import ( "encoding/json" "fmt" "net/http" "regexp" "strings" ) var ( cfRayPattern = regexp.MustCompile(`(?i)cf-ray[:\s=]+([a-z0-9-]+)`) cRayPattern = regexp.MustCompile(`(?i)cRay:\s*'([a-z0-9-]+)'`) htmlChallenge = []string{ "window._cf_chl_opt", "just a moment", "enable javascript and cookies to continue", "__cf_chl_", "challenge-platform", } ) // IsCloudflareChallengeResponse reports whether the upstream response matches Cloudflare challenge behavior. func IsCloudflareChallengeResponse(statusCode int, headers http.Header, body []byte) bool { if statusCode != http.StatusForbidden && statusCode != http.StatusTooManyRequests { return false } if headers != nil && strings.EqualFold(strings.TrimSpace(headers.Get("cf-mitigated")), "challenge") { return true } preview := strings.ToLower(TruncateBody(body, 4096)) for _, marker := range htmlChallenge { if strings.Contains(preview, marker) { return true } } contentType := "" if headers != nil { contentType = strings.ToLower(strings.TrimSpace(headers.Get("content-type"))) } if strings.Contains(contentType, "text/html") && (strings.Contains(preview, "= 2 { return strings.TrimSpace(matches[1]) } if matches := cRayPattern.FindStringSubmatch(preview); len(matches) >= 2 { return strings.TrimSpace(matches[1]) } return "" } // FormatCloudflareChallengeMessage appends cf-ray info when available. func FormatCloudflareChallengeMessage(base string, headers http.Header, body []byte) string { rayID := ExtractCloudflareRayID(headers, body) if rayID == "" { return base } return fmt.Sprintf("%s (cf-ray: %s)", base, rayID) } // ExtractUpstreamErrorCodeAndMessage extracts structured error code/message from common JSON layouts. func ExtractUpstreamErrorCodeAndMessage(body []byte) (string, string) { trimmed := strings.TrimSpace(string(body)) if trimmed == "" { return "", "" } if !json.Valid([]byte(trimmed)) { return "", truncateMessage(trimmed, 256) } var payload map[string]any if err := json.Unmarshal([]byte(trimmed), &payload); err != nil { return "", truncateMessage(trimmed, 256) } code := firstNonEmpty( extractNestedString(payload, "error", "code"), extractRootString(payload, "code"), ) message := firstNonEmpty( extractNestedString(payload, "error", "message"), extractRootString(payload, "message"), extractNestedString(payload, "error", "detail"), extractRootString(payload, "detail"), ) return strings.TrimSpace(code), truncateMessage(strings.TrimSpace(message), 512) } // TruncateBody truncates body text for logging/inspection. func TruncateBody(body []byte, max int) string { if max <= 0 { max = 512 } raw := strings.TrimSpace(string(body)) if len(raw) <= max { return raw } return raw[:max] + "...(truncated)" } func truncateMessage(s string, max int) string { if max <= 0 { return "" } if len(s) <= max { return s } return s[:max] + "...(truncated)" } func firstNonEmpty(values ...string) string { for _, v := range values { if strings.TrimSpace(v) != "" { return v } } return "" } func extractRootString(m map[string]any, key string) string { if m == nil { return "" } v, ok := m[key] if !ok { return "" } s, _ := v.(string) return s } func extractNestedString(m map[string]any, parent, key string) string { if m == nil { return "" } node, ok := m[parent] if !ok { return "" } child, ok := node.(map[string]any) if !ok { return "" } s, _ := child[key].(string) return s }