diff --git a/server-rs/crates/api-server/src/tripo3d/errors.rs b/server-rs/crates/api-server/src/tripo3d/errors.rs index 9970365e1..31159d583 100644 --- a/server-rs/crates/api-server/src/tripo3d/errors.rs +++ b/server-rs/crates/api-server/src/tripo3d/errors.rs @@ -48,25 +48,62 @@ pub(crate) fn map_pricing_store_error(error: EditorGenerationPricingError) -> Ap })) } -/// provider 错误归一:参数类回 400,其余按“上游不可用/上游拒绝”分流。 +/// provider 错误归一:参数类回 400,瞬时状态按可重试语义上报,其余按“上游不可用/上游拒绝”分流。 pub(crate) fn map_provider_error(error: TripoError) -> AppError { - let (status, reason) = match &error { - TripoError::InvalidParameters { .. } | TripoError::SdkInvalidArgument(_) => { - (StatusCode::BAD_REQUEST, "tripo-invalid-parameters") - } - TripoError::Api { status, .. } if matches!(status, Some(400..=499)) => { - (StatusCode::BAD_REQUEST, "tripo-api-rejected") - } - TripoError::TaskFailure { .. } => (StatusCode::BAD_GATEWAY, "tripo-task-failed"), - TripoError::OutputSchema { .. } => (StatusCode::BAD_GATEWAY, "tripo-output-schema"), - TripoError::Request { .. } | TripoError::Sdk { .. } | TripoError::Api { .. } => { - (StatusCode::BAD_GATEWAY, "tripo-upstream-error") - } + let (status, reason, message) = match &error { + TripoError::InvalidParameters { .. } | TripoError::SdkInvalidArgument(_) => ( + StatusCode::BAD_REQUEST, + "tripo-invalid-parameters", + error.to_string(), + ), + // 408 / 425 / 429 与 platform-tripo 的 is_retryable 口径一致,属于瞬时状态: + // 混进 400 会让客户端把限流 / 上游超时误读成自己参数写错了。 + TripoError::Api { + status: Some(429), .. + } => ( + StatusCode::TOO_MANY_REQUESTS, + "tripo-api-rate-limited", + "3D 生成服务当前限流,请稍后重试。".to_string(), + ), + TripoError::Api { + status: Some(408), .. + } => ( + StatusCode::GATEWAY_TIMEOUT, + "tripo-api-timeout", + "3D 生成服务响应超时,请稍后重试。".to_string(), + ), + TripoError::Api { + status: Some(425), .. + } => ( + StatusCode::BAD_GATEWAY, + "tripo-api-retry-later", + "3D 生成服务暂时无法处理该请求,请稍后重试。".to_string(), + ), + TripoError::Api { status, .. } if matches!(status, Some(400..=499)) => ( + StatusCode::BAD_REQUEST, + "tripo-api-rejected", + error.to_string(), + ), + TripoError::TaskFailure { .. } => ( + StatusCode::BAD_GATEWAY, + "tripo-task-failed", + error.to_string(), + ), + TripoError::OutputSchema { .. } => ( + StatusCode::BAD_GATEWAY, + "tripo-output-schema", + error.to_string(), + ), + TripoError::Request { .. } | TripoError::Sdk { .. } | TripoError::Api { .. } => ( + StatusCode::BAD_GATEWAY, + "tripo-upstream-error", + error.to_string(), + ), }; AppError::from_status(status).with_details(json!({ "provider": TRIPO_PROVIDER, "reason": reason, - "message": error.to_string(), + "message": message, })) } @@ -100,4 +137,33 @@ mod tests { }); assert_eq!(error.status_code(), StatusCode::BAD_GATEWAY); } + + /// 408 / 425 / 429 是 platform-tripo 认定的瞬时状态,不能按「参数有问题」回 400。 + #[test] + fn provider_transient_4xx_is_not_reported_as_bad_request() { + for (status, expected) in [ + (429u16, StatusCode::TOO_MANY_REQUESTS), + (408, StatusCode::GATEWAY_TIMEOUT), + (425, StatusCode::BAD_GATEWAY), + ] { + let error = map_provider_error(TripoError::Api { + code: 1001, + message: Some("transient".to_string()), + suggestion: None, + status: Some(status), + }); + assert_eq!(error.status_code(), expected, "status {status}"); + } + } + + #[test] + fn provider_client_side_4xx_stays_bad_request() { + let error = map_provider_error(TripoError::Api { + code: 1002, + message: Some("bad parameter".to_string()), + suggestion: None, + status: Some(400), + }); + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST); + } }