修复 BgFilter body-read 失败被误记为 502/5xx
response.chunk() 在 HTTP 状态已成功后中断,属于「读 body 时链路断裂」 的传输层故障,但原先只抛 BAD_GATEWAY+message,外部 API 失败审计取不到 transport/timeout/upstreamStatus,回落 Some(502) → statusClass 5xx, 漏掉了 transport 场景。 - 新增 editor_image_removal_body_read_error:timeout 走 504、其余 502, 统一带 transport/timeout/latencyMs/rawExcerpt,审计正确归类为 transport。 - read_editor_image_removal_response_bytes 加 request_started_at,BgFilter 与 birefnet 两条读体路径都透传,chunk 失败可算出 latencyMs。 - BgFilter 空 body 补 latencyMs,与 Aliyun 兜底口径对齐。 - 补两个回归测试覆盖 body-read 传输故障与 timeout 分类。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2476,12 +2476,16 @@ async fn request_editor_generated_screen_background_with_bgfilter(
|
||||
.get("x-bgfilter-screen-color")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(ToOwned::to_owned);
|
||||
let bytes = read_editor_image_removal_response_bytes(response, "bgfilter", "BgFilter").await?;
|
||||
let bytes =
|
||||
read_editor_image_removal_response_bytes(response, "bgfilter", "BgFilter", request_started_at)
|
||||
.await?;
|
||||
if bytes.is_empty() {
|
||||
// HTTP 已成功但 body 为空属于上游内容缺陷(5xx 归类正确),仍补 latencyMs 与 Aliyun 兜底口径对齐。
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "bgfilter",
|
||||
"message": "BgFilter 服务未返回图片",
|
||||
"latencyMs": request_started_at.elapsed().as_millis() as u64,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -2607,7 +2611,7 @@ async fn request_editor_background_removal_image(
|
||||
.get("x-birefnet-elapsed-ms")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<u64>().ok());
|
||||
let bytes = read_editor_background_removal_bytes(response).await?;
|
||||
let bytes = read_editor_background_removal_bytes(response, request_started_at).await?;
|
||||
if bytes.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
@@ -2723,14 +2727,17 @@ fn map_editor_bgfilter_error(error: reqwest::Error, latency_ms: u64) -> AppError
|
||||
|
||||
async fn read_editor_background_removal_bytes(
|
||||
response: reqwest::Response,
|
||||
request_started_at: Instant,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
read_editor_image_removal_response_bytes(response, "birefnet", "抠图服务").await
|
||||
read_editor_image_removal_response_bytes(response, "birefnet", "抠图服务", request_started_at)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_editor_image_removal_response_bytes(
|
||||
mut response: reqwest::Response,
|
||||
provider: &str,
|
||||
service_label: &str,
|
||||
request_started_at: Instant,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
if response.content_length().is_some_and(|content_length| {
|
||||
content_length > EDITOR_BACKGROUND_REMOVAL_MAX_RESPONSE_BYTES as u64
|
||||
@@ -2744,11 +2751,16 @@ async fn read_editor_image_removal_response_bytes(
|
||||
);
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
// response.chunk() 失败属于「HTTP 状态已成功、读 body 时链路断裂」的传输层故障,
|
||||
// 必须打上 transport/timeout/latencyMs/rawExcerpt,否则外部 API 失败审计会把它错记成 502/5xx。
|
||||
while let Some(chunk) = response.chunk().await.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": provider,
|
||||
"message": format!("读取{service_label}结果失败:{error}"),
|
||||
}))
|
||||
editor_image_removal_body_read_error(
|
||||
provider,
|
||||
service_label,
|
||||
error.is_timeout(),
|
||||
request_started_at.elapsed().as_millis() as u64,
|
||||
&error.to_string(),
|
||||
)
|
||||
})? {
|
||||
if bytes.len().saturating_add(chunk.len()) > EDITOR_BACKGROUND_REMOVAL_MAX_RESPONSE_BYTES {
|
||||
return Err(
|
||||
@@ -2764,6 +2776,31 @@ async fn read_editor_image_removal_response_bytes(
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// 读取抠图上游响应体失败(response.chunk 中断)时构造的传输层失败错误。
|
||||
/// 与 `map_editor_bgfilter_error` 对齐:timeout 走 504、其余 502,均带 transport 标记以及
|
||||
/// latencyMs / rawExcerpt,供外部 API 失败审计正确归类为 transport 而非 5xx。
|
||||
fn editor_image_removal_body_read_error(
|
||||
provider: &str,
|
||||
service_label: &str,
|
||||
is_timeout: bool,
|
||||
latency_ms: u64,
|
||||
error_text: &str,
|
||||
) -> AppError {
|
||||
let status = if is_timeout {
|
||||
StatusCode::GATEWAY_TIMEOUT
|
||||
} else {
|
||||
StatusCode::BAD_GATEWAY
|
||||
};
|
||||
AppError::from_status(status).with_details(json!({
|
||||
"provider": provider,
|
||||
"message": format!("读取{service_label}结果失败:{error_text}"),
|
||||
"timeout": is_timeout,
|
||||
"transport": true,
|
||||
"latencyMs": latency_ms,
|
||||
"rawExcerpt": error_text,
|
||||
}))
|
||||
}
|
||||
|
||||
fn decode_editor_background_removal_image(bytes: &[u8]) -> Result<image::DynamicImage, AppError> {
|
||||
decode_editor_removed_background_image(bytes, "birefnet")
|
||||
}
|
||||
@@ -5666,6 +5703,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgfilter_body_read_failure_audits_as_transport_not_5xx() {
|
||||
// HTTP 状态已成功、但读 body 时链路断裂:外部 API 失败审计必须归类为 transport,
|
||||
// 并保留 latencyMs / rawExcerpt,而不是被错记成 502/5xx。
|
||||
let error = editor_image_removal_body_read_error(
|
||||
"bgfilter",
|
||||
"BgFilter",
|
||||
false,
|
||||
137,
|
||||
"error reading a body from connection: connection reset",
|
||||
);
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_GATEWAY);
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&error),
|
||||
None,
|
||||
"body-read 传输故障不应带 HTTP statusCode,否则会落成 5xx"
|
||||
);
|
||||
assert!(!crate::external_api_audit::matting_failure_audit_timeout(&error));
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_latency_ms(&error),
|
||||
Some(137)
|
||||
);
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_raw_excerpt(&error).as_deref(),
|
||||
Some("error reading a body from connection: connection reset")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgfilter_body_read_timeout_maps_to_gateway_timeout_and_transport() {
|
||||
let error =
|
||||
editor_image_removal_body_read_error("bgfilter", "BgFilter", true, 42, "operation timed out");
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::GATEWAY_TIMEOUT);
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&error),
|
||||
None
|
||||
);
|
||||
assert!(crate::external_api_audit::matting_failure_audit_timeout(&error));
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_latency_ms(&error),
|
||||
Some(42)
|
||||
);
|
||||
}
|
||||
|
||||
fn manual_screen_background_decision(hex: &str) -> EditorScreenBackgroundDecision {
|
||||
EditorScreenBackgroundDecision {
|
||||
color: parse_editor_screen_background_color(Some(hex))
|
||||
|
||||
Reference in New Issue
Block a user