阿里云抠图本地预检失败不再误报为可重试供应商故障

InvalidConfig/InvalidRequest/Sign 都是发请求前的本地预检失败(未配置、图片解码失败、
尺寸过小、签名构造失败),此时根本没调用阿里云。旧映射统一默认 externalCallAttempted=true、
transport=true,把本地失败写成可重试的 transport 供应商故障审计。
现按 MattingError 变体区分:仅 Upstream 保留原上游失败语义,本地预检标记
externalCallAttempted=false / transport=false,调用方据此跳过外部失败审计。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 08:47:21 +00:00
parent fa64bfb475
commit cb1dd78e1d
@@ -4,6 +4,7 @@
//! 统一由 platform-matting 上传 VIAPI 官方临时桶(1 天自动过期,无需清理)。
use axum::http::StatusCode;
use platform_matting::MattingError;
use serde_json::json;
use crate::{
@@ -27,23 +28,7 @@ pub(crate) async fn segment_image_with_aliyun_matting(
.segment_image_to_transparent_png(image.bytes.as_slice(), &file_name)
.await
.map_err(|error| {
let message = error.message();
let timeout = aliyun_matting_error_is_timeout(message);
let upstream_status = aliyun_matting_error_http_status(message);
let status = if timeout {
StatusCode::GATEWAY_TIMEOUT
} else {
StatusCode::BAD_GATEWAY
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-matting",
"message": message,
"timeout": timeout,
"transport": aliyun_matting_error_is_transport(message),
"upstreamStatus": upstream_status,
"latencyMs": started_at.elapsed().as_millis() as u64,
"rawExcerpt": message.chars().take(500).collect::<String>(),
}))
aliyun_matting_failure_to_app_error(&error, started_at.elapsed().as_millis() as u64)
})?;
tracing::info!(
provider = "aliyun-matting",
@@ -59,6 +44,47 @@ pub(crate) async fn segment_image_with_aliyun_matting(
})
}
/// 把 platform-matting 的错误映射成审计友好的 AppError。
///
/// 关键区分:`InvalidConfig` / `InvalidRequest` / `Sign` 都是发请求前的本地预检失败
/// (客户端未配置、图片解码失败、尺寸过小、签名构造失败),此时根本没调用阿里云,必须
/// 标记 `externalCallAttempted=false`、`transport=false`,否则会被误写成可重试的供应商
/// transport 故障审计。只有 `Upstream` 才是真实发生过的外部调用失败。
fn aliyun_matting_failure_to_app_error(error: &MattingError, latency_ms: u64) -> AppError {
let message = error.message();
match error {
MattingError::Upstream(_) => {
let timeout = aliyun_matting_error_is_timeout(message);
let upstream_status = aliyun_matting_error_http_status(message);
let status = if timeout {
StatusCode::GATEWAY_TIMEOUT
} else {
StatusCode::BAD_GATEWAY
};
AppError::from_status(status).with_details(json!({
"provider": "aliyun-matting",
"message": message,
"timeout": timeout,
"transport": aliyun_matting_error_is_transport(message),
"upstreamStatus": upstream_status,
"latencyMs": latency_ms,
"rawExcerpt": message.chars().take(500).collect::<String>(),
}))
}
MattingError::InvalidConfig(_) | MattingError::InvalidRequest(_) | MattingError::Sign(_) => {
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
"provider": "aliyun-matting",
"message": message,
"timeout": false,
"transport": false,
"externalCallAttempted": false,
"latencyMs": latency_ms,
"rawExcerpt": message.chars().take(500).collect::<String>(),
}))
}
}
}
fn aliyun_matting_error_is_timeout(message: &str) -> bool {
let normalized = message.to_ascii_lowercase();
normalized.contains("timeout") || normalized.contains("timed out")
@@ -134,4 +160,40 @@ mod tests {
assert!(!crate::external_api_audit::matting_failure_external_call_attempted(&error));
}
#[test]
fn local_preflight_failures_do_not_count_as_external_call() {
for error in [
MattingError::InvalidRequest(
"待抠图图片尺寸 16x16 过小,阿里云通用抠图要求每条边大于 32 像素".to_string(),
),
MattingError::InvalidRequest("解析待抠图图片失败:invalid png".to_string()),
MattingError::InvalidConfig("endpoint 为空".to_string()),
MattingError::Sign("初始化 OSS V1 签名器失败".to_string()),
] {
let mapped = aliyun_matting_failure_to_app_error(&error, 3);
assert!(
!crate::external_api_audit::matting_failure_external_call_attempted(&mapped),
"本地预检失败不应记为外部调用:{}",
error.message()
);
let details = mapped.details().expect("details present");
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false));
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false));
}
}
#[test]
fn upstream_failure_still_counts_as_external_call() {
let error = MattingError::Upstream(
"通用抠图请求失败:dns error".to_string(),
);
let mapped = aliyun_matting_failure_to_app_error(&error, 12);
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
let details = mapped.details().expect("details present");
// 无 HTTP 状态的上游失败仍应标记为可重试 transport 故障。
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true));
}
}