传输判据补上 is_body,并把截断体的归因写清
- server-rs/crates/platform-tripo/src/common/error.rs:transport_error_is_transient 补上 is_body(),与 TripoError::Request.retryable 文档列举的瞬时类型对齐 - server-rs/crates/platform-tripo/src/common/error.rs:注释写明「传输中途截断的响应体在 reqwest 里落在 Decode」这一实测结论,说明为何不把 Decode 判成可重试 - server-rs/crates/platform-tripo/src/common/error.rs:补用例用截断响应体锁住 Decode 不判可重试
This commit is contained in:
@@ -195,6 +195,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 声明 64 字节、只给 4 字节就断开:读 body 会得到传输中断类错误。
|
||||
async fn spawn_truncated_body_server() -> std::net::SocketAddr {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("mock server 必须能监听");
|
||||
let addr = listener.local_addr().expect("mock server 必须有地址");
|
||||
tokio::spawn(async move {
|
||||
let Ok((mut socket, _)) = listener.accept().await else {
|
||||
return;
|
||||
};
|
||||
let mut request = [0u8; 1024];
|
||||
let _ = socket.read(&mut request).await;
|
||||
let head = "HTTP/1.1 200 OK\r\n\
|
||||
Content-Type: model/gltf-binary\r\n\
|
||||
Content-Length: 64\r\n\
|
||||
\r\n";
|
||||
if socket.write_all(head.as_bytes()).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = socket.write_all(b"glTF").await;
|
||||
let _ = socket.flush().await;
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
/// 是否可重试不再由「有没有 status」推断:同一个 `None` 既可能是瞬时传输失败,
|
||||
/// 也可能是「响应体格式不对」这种再试一次也一样的结果。
|
||||
#[test]
|
||||
@@ -253,6 +280,28 @@ mod tests {
|
||||
.expect_err("非法地址必须失败");
|
||||
assert!(!transport_error_is_transient(&malformed), "{malformed}");
|
||||
assert!(!sdk_request_is_retryable(None, Some(&malformed)));
|
||||
|
||||
// 传输中途截断的响应体:reqwest 归到 Decode(见上面的说明),这里刻意不判可重试,
|
||||
// 免得「提交请求的响应体解码失败」再发一次造成重复下单。
|
||||
let addr = spawn_truncated_body_server().await;
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!("http://{addr}/a.glb"))
|
||||
.send()
|
||||
.await
|
||||
.expect("响应头必须正常返回");
|
||||
let truncated = response
|
||||
.bytes()
|
||||
.await
|
||||
.expect_err("声明 64 字节却只给 4 字节必须失败");
|
||||
assert!(
|
||||
truncated.is_decode() && !truncated.is_body(),
|
||||
"截断的响应体应落在 Decode 而不是 Body:{truncated:?}"
|
||||
);
|
||||
assert!(
|
||||
!transport_error_is_transient(&truncated),
|
||||
"落在 Decode 的截断体不判可重试(提交请求重发会重复下单):{truncated}"
|
||||
);
|
||||
assert!(!sdk_request_is_retryable(None, Some(&truncated)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -376,9 +425,16 @@ pub(crate) fn http_status_is_transient(status: u16) -> bool {
|
||||
}
|
||||
|
||||
/// 传输层失败是否瞬时。`is_connect` 覆盖连接被拒 / 重置与握手阶段失败、`is_timeout`
|
||||
/// 覆盖连接与读写超时;其余(URL 非法、构建期配置错误)重试也是同一个结果。
|
||||
/// 覆盖连接与读写超时、`is_body` 覆盖块读取阶段的响应体传输中断 —— 对齐
|
||||
/// `TripoError::Request.retryable` 文档里列举的瞬时类型;其余(URL 非法、
|
||||
/// 构建期配置错误、响应体格式 / 解码失败)重试也是同一个结果。
|
||||
///
|
||||
/// 注意:传输中途截断的响应体在 reqwest 里落在 `Decode`(source 是
|
||||
/// `hyper::Error(Body, IncompleteBody)`),与真正的解码失败在 `reqwest::Error` 层面无法
|
||||
/// 区分,因此这里**不**把 `Decode` 判成可重试 —— 提交请求的响应体解码失败再发一次可能
|
||||
/// 重复下单。产物下载路径有单独的 `body_read_error`,由那条路径自己标记可重试。
|
||||
pub(crate) fn transport_error_is_transient(error: &reqwest::Error) -> bool {
|
||||
error.is_timeout() || error.is_connect()
|
||||
error.is_timeout() || error.is_connect() || error.is_body()
|
||||
}
|
||||
|
||||
/// SDK 的 `Error::Request` 是否值得重试。
|
||||
|
||||
Reference in New Issue
Block a user