From 6e081676f0138ab373729e8edba8b844b3a0d524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 24 Sep 2026 19:53:52 +0800 Subject: [PATCH] =?UTF-8?q?provider=20=E7=9A=84=204=20=E5=AD=97=E5=A4=B4?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=B8=80=E5=BE=8B=E6=8C=89=E3=80=8C=E4=BD=A0?= =?UTF-8?q?=E7=9A=84=E5=8F=82=E6=95=B0=E4=B8=8D=E5=AF=B9=E3=80=8D=E5=9B=9E?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api-server tripo3d/errors.rs:401/403 按上游凭据或权限不可用(502,`tripo-api-unauthorized`)、404 按上游找不到(502,`tripo-api-not-found`)、409 按上游冲突(409,`tripo-api-conflict`)、其余 4 字头按上游拒绝(502,`tripo-api-refused`);只有 400 / 422 继续回 400 `tripo-api-rejected` - 文件头部说明补上这条分档口径 - 用例:`provider_client_side_4xx_stays_bad_request` 覆盖 400 / 422;新增 `provider_non_parameter_4xx_is_not_reported_as_bad_request` 覆盖 401/403/404/409/402 的状态码、reason 与「不回传上游原文」 --- .../crates/api-server/src/tripo3d/errors.rs | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/server-rs/crates/api-server/src/tripo3d/errors.rs b/server-rs/crates/api-server/src/tripo3d/errors.rs index a68e88328..4266078d6 100644 --- a/server-rs/crates/api-server/src/tripo3d/errors.rs +++ b/server-rs/crates/api-server/src/tripo3d/errors.rs @@ -1,6 +1,8 @@ //! Tripo 生成对外错误的统一映射。 //! //! 映射目标只有三档:参数问题 400、幂等冲突 409、provider / 配置问题 5xx。 +//! 4 字头里只有 400 / 422 是「你的请求被拒」:401 / 403 是密钥或权限没配好、404 是找不到、 +//! 409 是上游冲突,都不按参数问题回 400,否则客户端会去改一个改不动的地方。 //! 任何分支都不把 provider task id、带签名的临时地址或 SDK 类型带进响应。 use axum::http::StatusCode; @@ -79,13 +81,51 @@ pub(crate) fn map_provider_error(error: TripoError) -> AppError { "tripo-api-retry-later", "3D 生成服务暂时无法处理该请求,请稍后重试。".to_string(), ), - TripoError::Api { status, .. } if matches!(status, Some(400..=499)) => ( + // 401 / 403 是 provider 侧密钥或调用权限没配好,不是调用方参数写错: + // 混进 400 会让客户端把服务端配置故障当成自己的问题去改参数。 + TripoError::Api { + status: Some(401 | 403), + .. + } => ( + StatusCode::BAD_GATEWAY, + "tripo-api-unauthorized", + "3D 生成服务的调用凭据或权限不可用,请联系支持。".to_string(), + ), + TripoError::Api { + status: Some(404), .. + } => ( + StatusCode::BAD_GATEWAY, + "tripo-api-not-found", + "3D 生成服务未找到请求的资源。".to_string(), + ), + TripoError::Api { + status: Some(409), .. + } => ( + StatusCode::CONFLICT, + "tripo-api-conflict", + "3D 生成服务报告请求冲突,请稍后重试。".to_string(), + ), + // 只有 400 / 422 真的是「参数被拒」,provider 的 code / message / suggestion 是原文 + // 转述的 HTTP 错误体,没有做过 URL 清洗:回给客户端可能带出带签名的地址等内部信息, + // 因此只给固定文案。 + TripoError::Api { + status: Some(400 | 422), + .. + } => ( StatusCode::BAD_REQUEST, "tripo-api-rejected", - // provider 的 code / message / suggestion 是原文转述的 HTTP 错误体,没有做过 - // URL 清洗:回给客户端可能带出带签名的地址等内部信息,因此只给固定文案。 "3D 生成请求被 provider 拒绝,请检查参数后重试。".to_string(), ), + // 其余 4 字头(402 余额、405 方法、412 前置条件……)既不是调用方参数写错,也不是 + // 瞬时状态:按上游错误上报,保留原文之外的固定文案。 + TripoError::Api { + status: Some(400..=499), + .. + } => ( + StatusCode::BAD_GATEWAY, + "tripo-api-refused", + "3D 生成服务暂时拒绝了该请求,请稍后重试。".to_string(), + ), // 以下都是 5xx:完整错误只进日志。`TripoError` 的 Display 会带上 provider task id, // 直接回给客户端会破坏本文件「不暴露 task id」的口径,因此对外只给固定文案。 TripoError::TaskFailure { .. } => { @@ -189,13 +229,41 @@ mod tests { #[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); + for status in [400u16, 422] { + let error = map_provider_error(TripoError::Api { + code: 1002, + message: Some("bad parameter".to_string()), + suggestion: None, + status: Some(status), + }); + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST, "{status}"); + assert_eq!(detail(&error, "reason"), Some(json!("tripo-api-rejected"))); + } + } + + /// 401 / 403 / 404 / 409 与其余 4 字头都不是「调用方参数不对」:按上游 / 冲突上报, + /// 且各自有独立 reason,运维能从响应里看出是配置、找不到还是冲突。 + #[test] + fn provider_non_parameter_4xx_is_not_reported_as_bad_request() { + for (status, expected_status, expected_reason) in [ + (401u16, StatusCode::BAD_GATEWAY, "tripo-api-unauthorized"), + (403, StatusCode::BAD_GATEWAY, "tripo-api-unauthorized"), + (404, StatusCode::BAD_GATEWAY, "tripo-api-not-found"), + (409, StatusCode::CONFLICT, "tripo-api-conflict"), + (402, StatusCode::BAD_GATEWAY, "tripo-api-refused"), + ] { + let error = map_provider_error(TripoError::Api { + code: 1003, + message: Some("secret-upstream-body".to_string()), + suggestion: None, + status: Some(status), + }); + assert_eq!(error.status_code(), expected_status, "status {status}"); + assert_eq!(detail(&error, "reason"), Some(json!(expected_reason))); + let serialized = serde_json::to_string(error.details().expect("details 必须存在")) + .expect("可序列化"); + assert!(!serialized.contains("secret-upstream-body"), "{serialized}"); + } } /// 本模块头部承诺「任何分支都不把 provider task id 带进响应」,502 分支必须守住。