继续收敛外围 HTTP 错误正文
统一画板、模板、图片分离和素材下载的状态码与安全正文 保留资源编辑、生成恢复、项目快照和错误报告的上游详情 补充精确脱敏测试与错误处理文档
This commit is contained in:
@@ -2907,7 +2907,12 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
|
||||
access.validate_frozen_session()?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("读取陶泥儿画布资源失败:HTTP {}", status.as_u16()));
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(crate::assets::external_asset_http_failure(
|
||||
"读取陶泥儿画布资源",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
|
||||
@@ -933,8 +933,8 @@ pub(crate) fn classify_external_generation_initial_response(
|
||||
))
|
||||
}
|
||||
_ => Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 平台图片生成返回未识别的成功状态 HTTP {}",
|
||||
status.as_u16()
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} {}",
|
||||
format_external_http_error("平台图片生成", status, &payload.to_string())
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1361,9 +1361,11 @@ pub(crate) async fn submit_external_generation_request(
|
||||
)
|
||||
})?;
|
||||
if response.status().is_server_error() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后收到 HTTP {},服务端是否已产生副作用未知",
|
||||
response.status().as_u16()
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 请求平台图片生成后收到 {},服务端是否已产生副作用未知",
|
||||
format_external_http_error("平台图片生成", status, &body)
|
||||
));
|
||||
}
|
||||
Ok(response)
|
||||
@@ -1394,8 +1396,8 @@ async fn resume_prepared_external_generation_at(
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
crate::platform_maintenance::watch_platform_response(status.as_u16(), &body);
|
||||
return Err(format!(
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} External Editor prepared 恢复提交返回 HTTP {};原生成账本已保留",
|
||||
status.as_u16()
|
||||
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} External Editor prepared 恢复提交返回 {};原生成账本已保留",
|
||||
format_external_http_error("External Editor prepared 恢复提交", status, &body)
|
||||
));
|
||||
}
|
||||
let submission_payload_result = response
|
||||
@@ -2137,9 +2139,12 @@ async fn upload_manifest_asset_remote_reference_at(
|
||||
.await
|
||||
.map_err(|error| format!("上传当前账号参考图失败:{error}"))?;
|
||||
if upload_response.status().as_u16() != ticket.success_action_status {
|
||||
return Err(format!(
|
||||
"上传当前账号参考图失败:HTTP {}",
|
||||
upload_response.status().as_u16()
|
||||
let status = upload_response.status();
|
||||
let body = upload_response.text().await.unwrap_or_default();
|
||||
return Err(crate::assets::external_asset_http_failure(
|
||||
"上传当前账号参考图",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
access.validate_frozen_session()?;
|
||||
|
||||
@@ -11,6 +11,60 @@ const PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_STORAGE_PREPARATION_FAILURE: &str =
|
||||
const PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_PERSISTENCE_FAILURE: &str =
|
||||
"private-external-editor-credential-persistence-failed";
|
||||
|
||||
/// Extract a bounded, redacted upstream error without treating a status code as the whole reason.
|
||||
fn external_asset_http_detail(body: &str) -> Option<String> {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(body).ok();
|
||||
let error = parsed
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("error"))
|
||||
.or_else(|| parsed.as_ref());
|
||||
let detail = ["message", "detail", "code", "additionalDetails"]
|
||||
.into_iter()
|
||||
.filter_map(|field| {
|
||||
error
|
||||
.and_then(|value| value.get(field))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
let detail = (!detail.is_empty()).then_some(detail).or_else(|| {
|
||||
let safe = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
body.trim(),
|
||||
600,
|
||||
);
|
||||
(!safe.trim().is_empty() && !matches!(safe.trim(), "{}" | "null" | "\"\"")).then_some(safe)
|
||||
})?;
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail,
|
||||
600,
|
||||
);
|
||||
(!detail.trim().is_empty()).then_some(detail)
|
||||
}
|
||||
|
||||
pub(crate) fn external_asset_http_failure(
|
||||
action: &str,
|
||||
status: reqwest::StatusCode,
|
||||
body: &str,
|
||||
) -> String {
|
||||
let detail = external_asset_http_detail(body)
|
||||
.map(|detail| format!(":{detail}"))
|
||||
.unwrap_or_default();
|
||||
match status {
|
||||
reqwest::StatusCode::UNAUTHORIZED => {
|
||||
format!("authentication-required: {action}失败(HTTP 401),请重新登录后重试{detail}")
|
||||
}
|
||||
reqwest::StatusCode::FORBIDDEN => {
|
||||
format!("permission-denied: {action}被服务器拒绝(HTTP 403){detail}")
|
||||
}
|
||||
_ => format!("{action}失败:HTTP {}{detail}", status.as_u16()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A task-scoped External Editor credential used only by the direct Codex
|
||||
/// runtime. It never changes the GUI account session and deliberately keeps
|
||||
/// the key private: callers may use it to make authenticated requests but may
|
||||
@@ -406,15 +460,11 @@ async fn create_private_external_editor_api_credentials_from_platform_session(
|
||||
if !status.is_success() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
crate::platform_maintenance::watch_platform_response(status.as_u16(), &body);
|
||||
return Err(match status {
|
||||
reqwest::StatusCode::UNAUTHORIZED => {
|
||||
"authentication-required: 陶泥儿登录已失效,无法创建本机开发者 Key".to_string()
|
||||
}
|
||||
reqwest::StatusCode::FORBIDDEN => {
|
||||
"permission-denied: 当前陶泥儿账号无权创建本机开发者 Key".to_string()
|
||||
}
|
||||
_ => format!("创建本机陶泥儿开发者 Key 失败:HTTP {}", status.as_u16()),
|
||||
});
|
||||
return Err(external_asset_http_failure(
|
||||
"创建本机陶泥儿开发者 Key",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -907,9 +957,10 @@ pub(crate) async fn sync_canvas_project_assets_at(
|
||||
if !project_status.is_success() {
|
||||
let body = project_response.text().await.unwrap_or_default();
|
||||
crate::platform_maintenance::watch_platform_response(project_status.as_u16(), &body);
|
||||
return Err(format!(
|
||||
"读取画板项目失败:HTTP {}",
|
||||
project_status.as_u16()
|
||||
return Err(external_asset_http_failure(
|
||||
"读取画板项目",
|
||||
project_status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
let project_payload = project_response
|
||||
@@ -1364,7 +1415,8 @@ where
|
||||
return Err("画板资产下载地址发生重定向,已拒绝继续请求".to_string());
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(format!("下载画板资产失败:HTTP {}", status.as_u16()));
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(external_asset_http_failure("下载画板资产", status, &body));
|
||||
}
|
||||
if response
|
||||
.content_length()
|
||||
@@ -1436,7 +1488,7 @@ pub(crate) async fn resolve_external_asset_signed_url(
|
||||
if !status.is_success() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
crate::platform_maintenance::watch_platform_response(status.as_u16(), &body);
|
||||
return Err(format!("换签画板资产失败:HTTP {}", status.as_u16()));
|
||||
return Err(external_asset_http_failure("换签画板资产", status, &body));
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -2257,6 +2309,19 @@ mod tests {
|
||||
use shared_contracts::game_creation_app::GameCreationAppAssetCategory;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn external_asset_http_failure_keeps_status_and_redacted_upstream_detail() {
|
||||
let detail = external_asset_http_failure(
|
||||
"读取画板项目",
|
||||
reqwest::StatusCode::UNAUTHORIZED,
|
||||
r#"{"error":{"code":"TOKEN_EXPIRED","message":"access token expired;authorization=Bearer secret"}}"#,
|
||||
);
|
||||
assert!(detail.contains("HTTP 401"), "{detail}");
|
||||
assert!(detail.contains("TOKEN_EXPIRED"), "{detail}");
|
||||
assert!(detail.contains("access token expired"), "{detail}");
|
||||
assert!(!detail.contains("Bearer secret"), "{detail}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_artifact_registration_reports_only_real_manifest_changes() {
|
||||
let temporary = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -3706,9 +3706,12 @@ async fn download_ui_editor_remote_asset(url: &str, max_bytes: u64) -> Result<Ve
|
||||
return Err("平台素材下载地址发生重定向,已拒绝继续请求".to_string());
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"下载平台素材失败:HTTP {}",
|
||||
response.status().as_u16()
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(crate::assets::external_asset_http_failure(
|
||||
"下载平台素材",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
if response
|
||||
|
||||
@@ -69,40 +69,66 @@ fn stable_submission_id(events: &[Event]) -> String {
|
||||
}
|
||||
|
||||
fn response_error(status: StatusCode, body: &str) -> String {
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
return "authentication-required: 陶泥儿登录态已过期,请重新登录后重试".to_string();
|
||||
}
|
||||
if status == StatusCode::FORBIDDEN {
|
||||
return "permission-denied: 当前账号没有提交错误报告的权限".to_string();
|
||||
}
|
||||
let detail = serde_json::from_str::<Value>(body)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
let error = value.get("error").unwrap_or(&value);
|
||||
error
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| {
|
||||
error
|
||||
.get("code")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
})
|
||||
.or_else(|| serde_json::to_string(error).ok())
|
||||
Some(
|
||||
["message", "detail", "code", "additionalDetails"]
|
||||
.into_iter()
|
||||
.filter_map(|field| {
|
||||
error
|
||||
.get(field)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(";"),
|
||||
)
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
.or_else(|| {
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
body.trim(),
|
||||
600,
|
||||
);
|
||||
(!detail.trim().is_empty()).then_some(detail)
|
||||
(!detail.trim().is_empty() && !matches!(detail.trim(), "{}" | "null" | "\"\""))
|
||||
.then_some(detail)
|
||||
})
|
||||
.unwrap_or_else(|| format!("HTTP {}", status.as_u16()));
|
||||
format!("错误报告提交失败:{detail}")
|
||||
.map(|detail| {
|
||||
crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
&detail,
|
||||
600,
|
||||
)
|
||||
})
|
||||
.filter(|value| !value.trim().is_empty());
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
return match detail {
|
||||
Some(detail) => {
|
||||
format!("authentication-required: 陶泥儿登录态已过期,请重新登录后重试:{detail}")
|
||||
}
|
||||
None => "authentication-required: 陶泥儿登录态已过期,请重新登录后重试".to_string(),
|
||||
};
|
||||
}
|
||||
if status == StatusCode::FORBIDDEN {
|
||||
return format!(
|
||||
"permission-denied: 当前账号没有提交错误报告的权限{}",
|
||||
detail
|
||||
.map(|detail| format!(":{detail}"))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"错误报告提交失败:HTTP {}{}",
|
||||
status.as_u16(),
|
||||
detail
|
||||
.map(|detail| format!(":{detail}"))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
|
||||
fn response_data(body: &str) -> Result<Value, String> {
|
||||
@@ -234,4 +260,16 @@ mod tests {
|
||||
assert_eq!(value["batchId"], "batch-1");
|
||||
assert!(!value.to_string().contains("token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_error_keeps_status_and_redacted_server_detail() {
|
||||
let detail = response_error(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
r#"{"error":{"code":"RATE_LIMITED","message":"too many requests;token=secret"}}"#,
|
||||
);
|
||||
assert!(detail.contains("HTTP 429"), "{detail}");
|
||||
assert!(detail.contains("RATE_LIMITED"), "{detail}");
|
||||
assert!(detail.contains("too many requests"), "{detail}");
|
||||
assert!(!detail.contains("token=secret"), "{detail}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,10 +376,13 @@ async fn upload_object(
|
||||
.await
|
||||
.map_err(|error| format!("上传素材失败:无法访问素材存储:{error};请检查网络后重试"))?;
|
||||
validate_session(snapshot)?;
|
||||
if response.status().as_u16() != ticket.success_action_status {
|
||||
return Err(format!(
|
||||
"上传素材到对象存储失败(HTTP {}),请重试",
|
||||
response.status().as_u16()
|
||||
let status = response.status();
|
||||
if status.as_u16() != ticket.success_action_status {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(crate::assets::external_asset_http_failure(
|
||||
"上传素材到对象存储",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1768,32 +1768,31 @@ async fn request_resource_edit_upload_ticket(
|
||||
.await
|
||||
.map_err(|error| format!("result-unknown: 创建源资源上传凭证请求失败:{error}"))?;
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(editor_api_authentication_error());
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authentication_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if response.status() == reqwest::StatusCode::FORBIDDEN {
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(editor_api_authorization_error());
|
||||
if status == reqwest::StatusCode::FORBIDDEN {
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authorization_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
if response.status().is_server_error() {
|
||||
return Err(format!(
|
||||
if !status.is_success() {
|
||||
let base = if status.is_server_error() {
|
||||
format!(
|
||||
"result-unknown: 创建源资源上传凭证返回 HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(format!(
|
||||
"创建源资源上传凭证失败:HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
status.as_u16()
|
||||
)
|
||||
} else if let Some(error) = post_response_session_error.as_ref() {
|
||||
error.clone()
|
||||
} else {
|
||||
format!("创建源资源上传凭证失败:HTTP {}", status.as_u16())
|
||||
};
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -1901,19 +1900,16 @@ async fn upload_resource_edit_source(
|
||||
.map_err(|error| format!("result-unknown: 上传源资源请求失败:{error}"))?;
|
||||
let post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status().as_u16() != ticket.success_action_status {
|
||||
let status = response.status();
|
||||
if response.status().is_server_error() {
|
||||
return Err(format!(
|
||||
"result-unknown: 上传源资源返回 HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
let base = format!("result-unknown: 上传源资源返回 HTTP {}", status.as_u16());
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
return Err(append_editor_api_reason(error, response).await);
|
||||
}
|
||||
return Err(format!(
|
||||
"上传源资源失败:HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
let base = format!("上传源资源失败:HTTP {}", status.as_u16());
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
Ok(ResourceEditRemoteStage {
|
||||
value: (),
|
||||
@@ -1955,32 +1951,31 @@ async fn confirm_resource_edit_source(
|
||||
.await
|
||||
.map_err(|error| format!("result-unknown: 确认源资源上传请求失败:{error}"))?;
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(editor_api_authentication_error());
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authentication_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if response.status() == reqwest::StatusCode::FORBIDDEN {
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(editor_api_authorization_error());
|
||||
if status == reqwest::StatusCode::FORBIDDEN {
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authorization_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
if response.status().is_server_error() {
|
||||
return Err(format!(
|
||||
if !status.is_success() {
|
||||
let base = if status.is_server_error() {
|
||||
format!(
|
||||
"result-unknown: 确认源资源上传返回 HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(format!(
|
||||
"确认源资源上传失败:HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
status.as_u16()
|
||||
)
|
||||
} else if let Some(error) = post_response_session_error.as_ref() {
|
||||
error.clone()
|
||||
} else {
|
||||
format!("确认源资源上传失败:HTTP {}", status.as_u16())
|
||||
};
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -2067,32 +2062,31 @@ async fn register_resource_edit_source_image(
|
||||
.await
|
||||
.map_err(|error| format!("result-unknown: 登记源图片项目资源请求失败:{error}"))?;
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(editor_api_authentication_error());
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authentication_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if response.status() == reqwest::StatusCode::FORBIDDEN {
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(editor_api_authorization_error());
|
||||
if status == reqwest::StatusCode::FORBIDDEN {
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authorization_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
if response.status().is_server_error() {
|
||||
return Err(format!(
|
||||
if !status.is_success() {
|
||||
let base = if status.is_server_error() {
|
||||
format!(
|
||||
"result-unknown: 登记源图片项目资源返回 HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
if let Some(error) = post_response_session_error {
|
||||
return Err(error);
|
||||
}
|
||||
return Err(format!(
|
||||
"登记源图片项目资源失败:HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
status.as_u16()
|
||||
)
|
||||
} else if let Some(error) = post_response_session_error.as_ref() {
|
||||
error.clone()
|
||||
} else {
|
||||
format!("登记源图片项目资源失败:HTTP {}", status.as_u16())
|
||||
};
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -2667,6 +2661,13 @@ async fn editor_api_rejection_reason(response: reqwest::Response) -> Option<Stri
|
||||
editor_api_rejection_reason_from_body(body.as_str())
|
||||
}
|
||||
|
||||
async fn append_editor_api_reason(base: String, response: reqwest::Response) -> String {
|
||||
match editor_api_rejection_reason(response).await {
|
||||
Some(reason) => format!("{base}:{reason}"),
|
||||
None => base,
|
||||
}
|
||||
}
|
||||
|
||||
/// 追加到用户可见原因里的外部字段上限:正文已经按 64KiB 收口,但单个字段仍可能把
|
||||
/// 「不让整段正文进 UI」的初衷抵消掉,所以每个追加项单独截断。
|
||||
const MAX_EDITOR_ERROR_DETAIL_CHARS: usize = 80;
|
||||
@@ -2788,16 +2789,16 @@ async fn submit_resource_edit_remote(
|
||||
let mut post_response_session_error = access.validate_frozen_session().err();
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if let Some(error) = post_response_session_error.as_ref() {
|
||||
return Err(error.clone());
|
||||
}
|
||||
return Err(editor_api_authentication_error());
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authentication_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if status == reqwest::StatusCode::FORBIDDEN {
|
||||
if let Some(error) = post_response_session_error.as_ref() {
|
||||
return Err(error.clone());
|
||||
}
|
||||
return Err(editor_api_authorization_error());
|
||||
let base = post_response_session_error
|
||||
.clone()
|
||||
.unwrap_or_else(editor_api_authorization_error);
|
||||
return Err(append_editor_api_reason(base, response).await);
|
||||
}
|
||||
if status == reqwest::StatusCode::BAD_REQUEST {
|
||||
ledger.terminal_failure_code = Some("remote-request-bad-request".to_string());
|
||||
@@ -2934,10 +2935,13 @@ async fn wait_for_resource_edit_remote(
|
||||
continue;
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
return Err(ResourceEditError::Other(format!(
|
||||
let base = format!(
|
||||
"result-unknown: 查询资源编辑任务返回 HTTP {}",
|
||||
response.status().as_u16()
|
||||
)));
|
||||
);
|
||||
return Err(ResourceEditError::Other(
|
||||
append_editor_api_reason(base, response).await,
|
||||
));
|
||||
}
|
||||
let payload = response
|
||||
.json::<serde_json::Value>()
|
||||
|
||||
@@ -323,7 +323,13 @@ async fn project_snapshot_response_error(
|
||||
let detail = response
|
||||
.text()
|
||||
.await
|
||||
.map(|body| body.chars().take(200).collect::<String>())
|
||||
.map(|body| {
|
||||
crate::agent::redact_agent_runtime_error(
|
||||
std::path::Path::new("__agc_no_project_root__"),
|
||||
&body,
|
||||
200,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let detail = detail.trim().to_string();
|
||||
if detail.is_empty() {
|
||||
|
||||
@@ -572,7 +572,13 @@ async fn fetch_limited_bytes(
|
||||
.await
|
||||
.map_err(|error| format!("请求模板库失败:{error}"))?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("模板库返回 HTTP {}", response.status().as_u16()));
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(crate::assets::external_asset_http_failure(
|
||||
"模板库请求",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
if response
|
||||
.content_length()
|
||||
|
||||
+8
-2
@@ -118,11 +118,17 @@ async fn raw_extract_inner(
|
||||
format!("图片分离请求失败:{error}")
|
||||
})?;
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
app_log!(
|
||||
"ui_separation.error stage=image_edit reason=http_status status={}",
|
||||
response.status()
|
||||
status
|
||||
);
|
||||
return Err(format!("图片分离请求失败(HTTP {})", response.status()));
|
||||
return Err(crate::assets::external_asset_http_failure(
|
||||
"图片分离请求",
|
||||
status,
|
||||
&body,
|
||||
));
|
||||
}
|
||||
let payload = response.json::<RawEditResponse>().await.map_err(|error| {
|
||||
app_log!("ui_separation.error stage=image_edit reason=parse_response error={error}");
|
||||
|
||||
@@ -131,6 +131,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- 外部 MCP loopback 的 401/429/桥上下文缺失必须返回带正文的 JSON 错误体(同时保留 HTTP 状态);不能让 Claude Code 侧只看到空 body 的状态码。
|
||||
- 认证 5xx、账户/素材上传/发行发布的 401/403 保留服务端 `message`/`code` 后再做精确脱敏;空 `{}`、`null` 等无信息响应不算错误正文。
|
||||
- 旧版已落盘的通用“执行通道中断”文本无法恢复历史原因,读取时明确追加“宿主未提供具体错误正文”;新事件只在确实没有 detail 时使用同样标记。
|
||||
- 画板/模板/图片分离/参考图/External Editor/资源编辑/项目快照/错误报告等外围 HTTP 入口也遵循同一出口:读取有限响应正文,保留状态码、服务端 message/code 和操作上下文,凭据、URL、路径按统一规则脱敏;不能因为不是 Direct 回合主链就回到“HTTP N”兜底。
|
||||
|
||||
## 后果与边界
|
||||
|
||||
|
||||
@@ -52,3 +52,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
|
||||
- 本轮继续扫出三类未收敛口:外部 MCP HTTP 的 401/429/桥上下文缺失原先只有空状态码响应,现改为保留 HTTP 状态、稳定错误码和中文正文;认证 5xx 结构化载荷、账户/素材上传/发行发布的 401/403 现保留服务端 message/code(仍精确脱敏)。
|
||||
- 前端历史错误文本此前会把含有敏感字段的整条 Direct 诊断直接丢弃,现改为只替换凭据、链接和路径;旧版已经落盘且无法恢复正文的“执行通道中断”条目会明确追加“宿主未提供具体错误正文”,不再伪装成当前仍在吞错。
|
||||
- 新增/通过:外部 MCP HTTP 401/429 定向 2 项、前端 Direct/Runtime 错误正文 25 项、旧历史通用中断脱敏回归 1 项;认证 Rust 定向 19 项、素材上传响应错误定向 1 项通过。`appSurface` 全套仍有环境/fixture 相关失败,不能据此宣称全绿。
|
||||
- 继续扫描又发现画板项目/资产下载、模板库、图片分离、参考图上传、External Editor 生成恢复、资源编辑上传/轮询、项目快照和错误报告提交仍有“HTTP 只有编号”或未脱敏正文路径;已统一补上状态码 + 有界正文 + 精确脱敏。资源编辑拒绝原因继续复用既有 `details.message` 解析,避免丢掉平台具体原因。
|
||||
|
||||
Reference in New Issue
Block a user