阿里云抠图本地预检失败不再误报为可重试供应商故障
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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user