原因:公开错误层将平台并发限流误标为上游限流,并把多种上游 4xx 统一压成 400,影响定位和客户端处理。 影响:新增公开错误 source,平台限流使用 gateway_rate_limited,上游请求按安全分类返回对应状态;数据库与管理端继续保留原始错误码、消息和状态用于审计。 验证:Go 全量测试、pnpm test、pnpm lint、pnpm build、pnpm openapi、gofmt 和 diff 检查均通过。
251 lines
12 KiB
Go
251 lines
12 KiB
Go
package publicerror
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const Version = "v1"
|
|
|
|
type Error struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Category string `json:"category"`
|
|
Source string `json:"source"`
|
|
HTTPStatus int `json:"httpStatus"`
|
|
Retryable bool `json:"retryable"`
|
|
Action string `json:"action"`
|
|
RetryAfterSeconds int `json:"retryAfterSeconds,omitempty"`
|
|
RequestID string `json:"requestId,omitempty"`
|
|
TaskID string `json:"taskId,omitempty"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
func FromFields(code string, message string, status int, retryable bool) Error {
|
|
originalCode := strings.TrimSpace(code)
|
|
code = strings.ToLower(originalCode)
|
|
message = strings.TrimSpace(message)
|
|
if status <= 0 {
|
|
status = statusFromCode(code)
|
|
}
|
|
if strings.HasPrefix(originalCode, "GATEWAY_") {
|
|
return newError(originalCode, safeRequestMessage(message), "gateway", status, retryable, retryAction(retryable))
|
|
}
|
|
if mapped, ok := mappedError(code, message, status, retryable); ok {
|
|
return mapped
|
|
}
|
|
if sensitiveTransportMessage(message) {
|
|
return newError("upstream_connection_interrupted", "The upstream connection was interrupted before a complete response was received.", "upstream", http.StatusBadGateway, true, "retry")
|
|
}
|
|
if code == "" {
|
|
code = defaultCodeForStatus(status)
|
|
} else {
|
|
code = originalCode
|
|
}
|
|
if message == "" {
|
|
message = defaultMessageForStatus(status)
|
|
}
|
|
category := "gateway"
|
|
action := "none"
|
|
if status == http.StatusTooManyRequests {
|
|
category, action, retryable = "rate_limit", "retry_after", true
|
|
} else if status >= 500 {
|
|
category = "upstream"
|
|
if retryable {
|
|
action = "retry"
|
|
}
|
|
} else if status >= 400 {
|
|
category, action = "request", "fix_request"
|
|
}
|
|
return newError(code, message, category, status, retryable, action)
|
|
}
|
|
|
|
func WithIDs(value Error, requestID string, taskID string) Error {
|
|
value.RequestID = strings.TrimSpace(requestID)
|
|
value.TaskID = strings.TrimSpace(taskID)
|
|
return value
|
|
}
|
|
|
|
func mappedError(code string, message string, status int, retryable bool) (Error, bool) {
|
|
switch code {
|
|
case "bad_request", "invalid_request", "invalid_parameter", "unsupported_kind", "unsupported_operation", "unsupported_response_parameter":
|
|
return newError(code, safeRequestMessage(message), "request", http.StatusBadRequest, false, "fix_request"), true
|
|
case "cancelled", "task_cancelled":
|
|
return newError(code, safeRequestMessage(message), "request", http.StatusConflict, false, "none"), true
|
|
case "validation_in_progress":
|
|
return newError(code, firstNonEmptyMessage(message, "New production tasks are paused while validation is running."), "gateway", http.StatusServiceUnavailable, true, "retry_after"), true
|
|
case "traffic_gate_unavailable":
|
|
return newError(code, "The gateway traffic admission service is temporarily unavailable.", "gateway", http.StatusServiceUnavailable, true, "retry"), true
|
|
case "response_read_error", "stream_read_error", "network", "connection_reset", "upstream_connection_interrupted", "unexpected_eof", "http2_stream_closed", "request_asset_fetch_failed":
|
|
return newError("upstream_connection_interrupted", "The upstream connection was interrupted before a complete response was received.", "upstream", http.StatusBadGateway, true, "retry"), true
|
|
case "upstream_timeout", "timeout", "context_deadline_exceeded":
|
|
return newError("upstream_timeout", "The upstream service did not respond in time.", "upstream", http.StatusGatewayTimeout, true, "retry"), true
|
|
case "gateway_rate_limited":
|
|
return newError("gateway_rate_limited", "The gateway rate limit was reached.", "rate_limit", http.StatusTooManyRequests, true, "retry_after"), true
|
|
case "rate_limit", "upstream_rate_limited", "too_many_requests":
|
|
return newError("upstream_rate_limited", "The upstream service rate limit was reached.", "rate_limit", http.StatusTooManyRequests, true, "retry_after"), true
|
|
case "upstream_unavailable", "upstream_overloaded", "service_unavailable", "bad_gateway", "server_error", "provider_failed":
|
|
if status > 0 && status < 500 {
|
|
break
|
|
}
|
|
return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
|
|
case "upload_invalid_response", "invalid_response", "response_too_large", "invalid_upstream_response", "upstream_invalid_response":
|
|
return newError("upstream_invalid_response", "The upstream service returned an invalid or incomplete response.", "upstream", http.StatusBadGateway, true, "retry"), true
|
|
case "invalid_api_key", "authentication_error", "auth_failed", "missing_credentials", "upstream_auth_failed":
|
|
return newError("upstream_auth_failed", "The upstream service rejected the configured credentials.", "upstream", http.StatusBadGateway, false, "contact_support"), true
|
|
case "storage_write_failed", "storage_config_invalid", "storage_auth_failed", "upload_config_failed", "upload_no_channel", "upload_network", "upload_read_failed", "upload_failed", "local_result_storage_unavailable", "local_static_store_failed", "request_asset_upload_failed":
|
|
return newError("storage_write_failed", "The media asset could not be written to object storage.", "storage", http.StatusServiceUnavailable, true, "retry"), true
|
|
case "storage_read_failed", "upload_source_fetch_failed", "upload_source_read_failed":
|
|
return newError("storage_read_failed", "The media asset could not be read from object storage.", "storage", http.StatusServiceUnavailable, true, "retry"), true
|
|
case "binary_result_expired", "result_expired":
|
|
return newError("result_expired", "The generated result has expired and must be submitted again.", "storage", http.StatusGone, false, "resubmit"), true
|
|
case "result_unavailable", "request_asset_expired":
|
|
return newError("result_unavailable", "The generated result is no longer available and must be submitted again.", "storage", http.StatusGone, false, "resubmit"), true
|
|
case "binary_result_corrupted", "result_binary_not_materialized":
|
|
return newError("result_corrupted", "The stored result failed an integrity check.", "storage", http.StatusInternalServerError, false, "contact_support"), true
|
|
}
|
|
if status == http.StatusTooManyRequests {
|
|
return newError("upstream_rate_limited", "The upstream service rate limit was reached.", "rate_limit", status, true, "retry_after"), true
|
|
}
|
|
if (status == http.StatusUnauthorized || status == http.StatusForbidden) && (strings.HasPrefix(code, "http_") || strings.Contains(code, "provider")) {
|
|
return newError("upstream_auth_failed", "The upstream service rejected the configured credentials.", "upstream", http.StatusBadGateway, false, "contact_support"), true
|
|
}
|
|
if strings.HasPrefix(code, "http_") && status >= 500 {
|
|
return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
|
|
}
|
|
if strings.HasPrefix(code, "http_") || (code == "provider_failed" && status > 0 && status < 500) {
|
|
return upstreamHTTPError(status), true
|
|
}
|
|
if status >= 500 && code != "upstream_submission_unknown" && (strings.Contains(code, "upstream") || strings.Contains(code, "provider")) {
|
|
return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
|
|
}
|
|
if status >= 500 && code != "upstream_submission_unknown" {
|
|
return newError("gateway_error", defaultMessageForStatus(status), "gateway", status, retryable, retryAction(retryable)), true
|
|
}
|
|
return Error{}, false
|
|
}
|
|
|
|
func safeRequestMessage(message string) string {
|
|
message = strings.TrimSpace(message)
|
|
if message == "" || sensitiveTransportMessage(message) {
|
|
return "The request parameters are invalid."
|
|
}
|
|
return message
|
|
}
|
|
|
|
func firstNonEmptyMessage(message string, fallback string) string {
|
|
if message = strings.TrimSpace(message); message != "" && !sensitiveTransportMessage(message) {
|
|
return message
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func retryAction(retryable bool) string {
|
|
if retryable {
|
|
return "retry"
|
|
}
|
|
return "contact_support"
|
|
}
|
|
|
|
func newError(code string, message string, category string, status int, retryable bool, action string) Error {
|
|
return Error{Code: code, Message: message, Category: category, Source: errorSource(code, category), HTTPStatus: status, Retryable: retryable, Action: action, Version: Version}
|
|
}
|
|
|
|
func upstreamHTTPError(status int) Error {
|
|
if status < 400 || status >= 500 {
|
|
status = http.StatusBadRequest
|
|
}
|
|
switch status {
|
|
case http.StatusBadRequest:
|
|
return newError("upstream_invalid_request", "The upstream service rejected the request parameters.", "upstream", status, false, "fix_request")
|
|
case http.StatusNotFound:
|
|
return newError("upstream_not_found", "The upstream service could not find the requested resource.", "upstream", status, false, "contact_support")
|
|
case http.StatusMethodNotAllowed:
|
|
return newError("upstream_method_not_allowed", "The upstream service did not accept the configured request method.", "upstream", status, false, "contact_support")
|
|
case http.StatusConflict:
|
|
return newError("upstream_conflict", "The upstream service reported a request conflict.", "upstream", status, false, "fix_request")
|
|
case http.StatusRequestEntityTooLarge:
|
|
return newError("upstream_payload_too_large", "The upstream service rejected the request because its payload was too large.", "upstream", status, false, "fix_request")
|
|
case http.StatusUnsupportedMediaType:
|
|
return newError("upstream_unsupported_media_type", "The upstream service rejected the request media type.", "upstream", status, false, "fix_request")
|
|
case http.StatusUnprocessableEntity:
|
|
return newError("upstream_unprocessable_request", "The upstream service could not process the request parameters.", "upstream", status, false, "fix_request")
|
|
default:
|
|
return newError("upstream_request_rejected", "The upstream service rejected the request.", "upstream", status, false, "fix_request")
|
|
}
|
|
}
|
|
|
|
func errorSource(code string, category string) string {
|
|
code = strings.ToLower(strings.TrimSpace(code))
|
|
switch {
|
|
case strings.HasPrefix(code, "upstream_"):
|
|
return "upstream"
|
|
case strings.HasPrefix(code, "gateway_"), category == "gateway", category == "storage":
|
|
return "gateway"
|
|
case category == "request":
|
|
return "client"
|
|
default:
|
|
return "gateway"
|
|
}
|
|
}
|
|
|
|
func sensitiveTransportMessage(message string) bool {
|
|
value := strings.ToLower(message)
|
|
for _, marker := range []string{"read tcp ", "write tcp ", "dial tcp ", "connection reset by peer", "unexpected eof", "http2:", "stream error", "context deadline exceeded"} {
|
|
if strings.Contains(value, marker) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func statusFromCode(code string) int {
|
|
if strings.HasPrefix(code, "http_") {
|
|
if status, err := strconv.Atoi(strings.TrimPrefix(code, "http_")); err == nil && status >= 400 && status <= 599 {
|
|
return status
|
|
}
|
|
}
|
|
switch code {
|
|
case "gateway_rate_limited", "rate_limit", "upstream_rate_limited", "too_many_requests":
|
|
return http.StatusTooManyRequests
|
|
case "upstream_timeout", "timeout", "context_deadline_exceeded":
|
|
return http.StatusGatewayTimeout
|
|
case "binary_result_expired", "result_expired", "result_unavailable", "request_asset_expired":
|
|
return http.StatusGone
|
|
case "storage_write_failed", "storage_read_failed", "local_result_storage_unavailable":
|
|
return http.StatusServiceUnavailable
|
|
default:
|
|
return http.StatusBadGateway
|
|
}
|
|
}
|
|
|
|
func defaultCodeForStatus(status int) string {
|
|
switch status {
|
|
case http.StatusBadRequest:
|
|
return "invalid_request"
|
|
case http.StatusUnauthorized:
|
|
return "unauthorized"
|
|
case http.StatusForbidden:
|
|
return "forbidden"
|
|
case http.StatusNotFound:
|
|
return "not_found"
|
|
case http.StatusConflict:
|
|
return "conflict"
|
|
case http.StatusTooManyRequests:
|
|
return "rate_limited"
|
|
default:
|
|
if status >= 500 {
|
|
return "gateway_error"
|
|
}
|
|
return "request_failed"
|
|
}
|
|
}
|
|
|
|
func defaultMessageForStatus(status int) string {
|
|
if value := http.StatusText(status); value != "" {
|
|
return value
|
|
}
|
|
return "Request failed."
|
|
}
|