修复抠图外部失败审计分类
保留 BgFilter 上游真实 HTTP 状态与超时标记 保留阿里云抠图上游状态、超时与错误摘要 补充审计分类单测覆盖 429 与 timeout 场景
This commit is contained in:
@@ -30,9 +30,19 @@ pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
.segment_image_to_transparent_png(image.bytes.as_slice(), &file_name)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
let message = error.message();
|
||||
let timeout = aliyun_matting_error_is_timeout(message);
|
||||
let status = if timeout {
|
||||
StatusCode::GATEWAY_TIMEOUT
|
||||
} else {
|
||||
StatusCode::BAD_GATEWAY
|
||||
};
|
||||
AppError::from_status(status).with_details(json!({
|
||||
"provider": "aliyun-matting",
|
||||
"message": error.message(),
|
||||
"message": message,
|
||||
"timeout": timeout,
|
||||
"upstreamStatus": aliyun_matting_error_http_status(message),
|
||||
"rawExcerpt": message.chars().take(500).collect::<String>(),
|
||||
}))
|
||||
})?;
|
||||
tracing::info!(
|
||||
@@ -48,3 +58,51 @@ pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn aliyun_matting_error_is_timeout(message: &str) -> bool {
|
||||
let normalized = message.to_ascii_lowercase();
|
||||
normalized.contains("timeout") || normalized.contains("timed out")
|
||||
}
|
||||
|
||||
fn aliyun_matting_error_http_status(message: &str) -> Option<u16> {
|
||||
let marker = "HTTP ";
|
||||
let start = message.find(marker)? + marker.len();
|
||||
let digits = message[start..]
|
||||
.chars()
|
||||
.take_while(|value| value.is_ascii_digit())
|
||||
.collect::<String>();
|
||||
digits.parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn aliyun_matting_error_http_status_extracts_provider_status() {
|
||||
assert_eq!(
|
||||
aliyun_matting_error_http_status(
|
||||
"通用抠图接口返回失败(HTTP 429,Code=Throttled):QPS exceeded"
|
||||
),
|
||||
Some(429)
|
||||
);
|
||||
assert_eq!(
|
||||
aliyun_matting_error_http_status("下载抠图结果失败(HTTP 403)"),
|
||||
Some(403)
|
||||
);
|
||||
assert_eq!(aliyun_matting_error_http_status("网络失败"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliyun_matting_error_timeout_detects_transport_timeout() {
|
||||
assert!(aliyun_matting_error_is_timeout(
|
||||
"通用抠图请求失败:operation timed out"
|
||||
));
|
||||
assert!(aliyun_matting_error_is_timeout(
|
||||
"GetOssStsToken 请求失败:request timeout"
|
||||
));
|
||||
assert!(!aliyun_matting_error_is_timeout(
|
||||
"通用抠图接口返回失败(HTTP 400,Code=InvalidImage):bad image"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2245,9 +2245,11 @@ async fn remove_editor_character_animation_frame_backgrounds(
|
||||
state.config.aliyun_matting_endpoint.clone(),
|
||||
"editor-character-animation-frame-matting",
|
||||
"aliyun_segment",
|
||||
error.status_code().as_u16(),
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&error),
|
||||
crate::external_api_audit::matting_failure_audit_timeout(&error),
|
||||
error.message().to_string(),
|
||||
Some(format!("frame_index={frame_index}")),
|
||||
crate::external_api_audit::matting_failure_audit_raw_excerpt(&error)
|
||||
.or_else(|| Some(format!("frame_index={frame_index}"))),
|
||||
)
|
||||
.await;
|
||||
remove_editor_generated_green_screen_background(&image, screen_color)?
|
||||
|
||||
@@ -2266,9 +2266,10 @@ async fn remove_editor_generated_screen_background_with_bgfilter(
|
||||
state.config.editor_bgfilter_base_url.clone(),
|
||||
"editor-screen-background-removal",
|
||||
"bgfilter_segment",
|
||||
error.status_code().as_u16(),
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&error),
|
||||
crate::external_api_audit::matting_failure_audit_timeout(&error),
|
||||
error.message().to_string(),
|
||||
None,
|
||||
crate::external_api_audit::matting_failure_audit_raw_excerpt(&error),
|
||||
)
|
||||
.await;
|
||||
match crate::aliyun_matting::segment_image_with_aliyun_matting(
|
||||
@@ -2294,9 +2295,10 @@ async fn remove_editor_generated_screen_background_with_bgfilter(
|
||||
state.config.aliyun_matting_endpoint.clone(),
|
||||
"editor-screen-background-removal",
|
||||
"aliyun_segment",
|
||||
error.status_code().as_u16(),
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&error),
|
||||
crate::external_api_audit::matting_failure_audit_timeout(&error),
|
||||
error.message().to_string(),
|
||||
None,
|
||||
crate::external_api_audit::matting_failure_audit_raw_excerpt(&error),
|
||||
)
|
||||
.await;
|
||||
remove_editor_generated_green_screen_background(image, screen_color)
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde_json::{Value, json};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{state::AppState, tracking::TrackingEventDraft};
|
||||
use crate::{http_error::AppError, state::AppState, tracking::TrackingEventDraft};
|
||||
|
||||
pub(crate) const EXTERNAL_API_FAILURE_EVENT_KEY: &str = "external_api_call_failure";
|
||||
pub(crate) const EXTERNAL_API_AUDIT_MODULE_KEY: &str = "external-api";
|
||||
@@ -156,29 +156,63 @@ pub(crate) async fn record_matting_external_api_failure(
|
||||
endpoint: String,
|
||||
operation: &'static str,
|
||||
failure_stage: &'static str,
|
||||
status_code: u16,
|
||||
status_code: Option<u16>,
|
||||
timeout: bool,
|
||||
error_message: String,
|
||||
raw_excerpt: Option<String>,
|
||||
) {
|
||||
let draft = ExternalApiFailureDraft::new(
|
||||
provider,
|
||||
endpoint,
|
||||
operation,
|
||||
failure_stage,
|
||||
error_message,
|
||||
)
|
||||
.with_status_code(Some(status_code))
|
||||
.with_optional_status_class(Some(status_class(Some(status_code))))
|
||||
.with_retryable(is_retryable_external_api_failure(
|
||||
Some(status_code),
|
||||
false,
|
||||
false,
|
||||
))
|
||||
.with_raw_excerpt(raw_excerpt)
|
||||
.with_audit_context(context);
|
||||
let draft =
|
||||
ExternalApiFailureDraft::new(provider, endpoint, operation, failure_stage, error_message)
|
||||
.with_status_code(status_code)
|
||||
.with_optional_status_class(Some(status_class(status_code)))
|
||||
.with_timeout(timeout)
|
||||
.with_retryable(is_retryable_external_api_failure(
|
||||
status_code,
|
||||
timeout,
|
||||
false,
|
||||
))
|
||||
.with_raw_excerpt(raw_excerpt)
|
||||
.with_audit_context(context);
|
||||
record_external_api_failure(state, draft).await;
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_status_code(error: &AppError) -> Option<u16> {
|
||||
error
|
||||
.details()
|
||||
.and_then(|details| details.get("upstreamStatus"))
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|value| u16::try_from(value).ok())
|
||||
.or_else(|| {
|
||||
if matting_failure_audit_timeout(error) {
|
||||
None
|
||||
} else {
|
||||
Some(error.status_code().as_u16())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_timeout(error: &AppError) -> bool {
|
||||
error
|
||||
.details()
|
||||
.and_then(|details| details.get("timeout"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_raw_excerpt(error: &AppError) -> Option<String> {
|
||||
error
|
||||
.details()
|
||||
.and_then(|details| {
|
||||
details
|
||||
.get("upstreamMessage")
|
||||
.or_else(|| details.get("rawExcerpt"))
|
||||
})
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.chars().take(800).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn build_external_api_failure_draft_from_platform_image_audit(
|
||||
audit: &PlatformImageFailureAudit,
|
||||
) -> ExternalApiFailureDraft {
|
||||
@@ -499,4 +533,75 @@ mod tests {
|
||||
assert_eq!(draft.metadata["statusCode"], 200);
|
||||
assert_eq!(draft.metadata["statusClass"], "5xx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matting_failure_audit_uses_upstream_status_instead_of_wrapped_status() {
|
||||
let error = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "bgfilter",
|
||||
"message": "BgFilter 服务返回非成功状态",
|
||||
"upstreamStatus": 429,
|
||||
"upstreamMessage": "too many requests",
|
||||
}));
|
||||
|
||||
let status_code = matting_failure_audit_status_code(&error);
|
||||
let timeout = matting_failure_audit_timeout(&error);
|
||||
let tracking = build_external_api_failure_tracking_draft(
|
||||
&ExternalApiFailureDraft::new(
|
||||
"bgfilter",
|
||||
"https://bgfilter.example/remove-background",
|
||||
"editor-screen-background-removal",
|
||||
"bgfilter_segment",
|
||||
error.message(),
|
||||
)
|
||||
.with_status_code(status_code)
|
||||
.with_optional_status_class(Some(status_class(status_code)))
|
||||
.with_timeout(timeout)
|
||||
.with_retryable(is_retryable_external_api_failure(
|
||||
status_code,
|
||||
timeout,
|
||||
false,
|
||||
))
|
||||
.with_raw_excerpt(matting_failure_audit_raw_excerpt(&error)),
|
||||
);
|
||||
|
||||
assert_eq!(tracking.metadata["statusCode"], 429);
|
||||
assert_eq!(tracking.metadata["statusClass"], "4xx");
|
||||
assert_eq!(tracking.metadata["timeout"], false);
|
||||
assert_eq!(tracking.metadata["retryable"], true);
|
||||
assert_eq!(tracking.metadata["rawExcerpt"], "too many requests");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matting_failure_audit_keeps_transport_timeout_classification() {
|
||||
let error = AppError::from_status(StatusCode::GATEWAY_TIMEOUT).with_details(json!({
|
||||
"provider": "bgfilter",
|
||||
"message": "请求 BgFilter 服务失败:operation timed out",
|
||||
"timeout": true,
|
||||
}));
|
||||
|
||||
let status_code = matting_failure_audit_status_code(&error);
|
||||
let timeout = matting_failure_audit_timeout(&error);
|
||||
let tracking = build_external_api_failure_tracking_draft(
|
||||
&ExternalApiFailureDraft::new(
|
||||
"bgfilter",
|
||||
"https://bgfilter.example/remove-background",
|
||||
"editor-screen-background-removal",
|
||||
"bgfilter_segment",
|
||||
error.message(),
|
||||
)
|
||||
.with_status_code(status_code)
|
||||
.with_optional_status_class(Some(status_class(status_code)))
|
||||
.with_timeout(timeout)
|
||||
.with_retryable(is_retryable_external_api_failure(
|
||||
status_code,
|
||||
timeout,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
assert_eq!(tracking.metadata["statusCode"], Value::Null);
|
||||
assert_eq!(tracking.metadata["statusClass"], "transport");
|
||||
assert_eq!(tracking.metadata["timeout"], true);
|
||||
assert_eq!(tracking.metadata["retryable"], true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user