Idempotency-Key 校验错误保留请求上下文

- server-rs/crates/api-server/src/tripo3d/routes.rs:require_idempotency_key 增加 request_context 参数,缺失 / 格式非法的两个 400 改用 into_response_with_context(Some(..)),与同处理器其它校验、定价错误一致;此前传 None,客户端拿不到请求 ID / 关联标识

验证:cargo test -p api-server --locked idempotency_key(相关 6 个用例全绿)
This commit is contained in:
2026-09-22 13:42:34 +08:00
parent 14740e0ef6
commit 326729a5ac
@@ -58,7 +58,7 @@ pub(crate) async fn submit_text_to_model(
payload: Result<Json<Model3dTextToModelRequest>, JsonRejection>,
) -> Result<(StatusCode, Json<serde_json::Value>), Response> {
let payload = parse_json_payload(&request_context, payload)?;
let idempotency_key = require_idempotency_key(&headers)?;
let idempotency_key = require_idempotency_key(&headers, &request_context)?;
let query = validate_text_to_model_request(&payload)
.map_err(map_request_error)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
@@ -86,7 +86,7 @@ pub(crate) async fn submit_image_to_model(
payload: Result<Json<Model3dImageToModelRequest>, JsonRejection>,
) -> Result<(StatusCode, Json<serde_json::Value>), Response> {
let payload = parse_json_payload(&request_context, payload)?;
let idempotency_key = require_idempotency_key(&headers)?;
let idempotency_key = require_idempotency_key(&headers, &request_context)?;
let query = validate_image_to_model_request(&payload)
.map_err(map_request_error)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
@@ -172,7 +172,10 @@ fn target_source_entity_id(
}
/// 两个提交都必须显式给出 `Idempotency-Key`;缺失或格式非法一律 400,不静默生成键。
fn require_idempotency_key(headers: &HeaderMap) -> Result<&str, Response> {
fn require_idempotency_key<'a>(
headers: &'a HeaderMap,
request_context: &RequestContext,
) -> Result<&'a str, Response> {
let value = headers
.get("idempotency-key")
.and_then(|value| value.to_str().ok())
@@ -184,7 +187,7 @@ fn require_idempotency_key(headers: &HeaderMap) -> Result<&str, Response> {
"provider": super::provider::TRIPO_PROVIDER,
"message": "3D 生成提交必须携带 Idempotency-Key 请求头。",
}))
.into_response_with_context(None)
.into_response_with_context(Some(request_context))
})?;
if value.len() > 128 || !value.bytes().all(|byte| (0x21..=0x7e).contains(&byte)) {
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
@@ -192,7 +195,7 @@ fn require_idempotency_key(headers: &HeaderMap) -> Result<&str, Response> {
"provider": super::provider::TRIPO_PROVIDER,
"message": "Idempotency-Key 必须是 1-128 个可打印 ASCII 字符,且不能包含空格。",
}))
.into_response_with_context(None));
.into_response_with_context(Some(request_context)));
}
Ok(value)
}
@@ -215,23 +218,29 @@ mod tests {
#[test]
fn idempotency_key_is_required_and_validated() {
let request_context = RequestContext::new(
"request-idempotency-key".to_string(),
"test".to_string(),
std::time::Duration::ZERO,
false,
);
let missing = HeaderMap::new();
assert!(require_idempotency_key(&missing).is_err());
assert!(require_idempotency_key(&missing, &request_context).is_err());
let mut headers = HeaderMap::new();
headers.insert("idempotency-key", HeaderValue::from_static("issue-1"));
assert_eq!(
require_idempotency_key(&headers).expect("合法键应通过"),
require_idempotency_key(&headers, &request_context).expect("合法键应通过"),
"issue-1"
);
let mut blank = HeaderMap::new();
blank.insert("idempotency-key", HeaderValue::from_static(" "));
assert!(require_idempotency_key(&blank).is_err());
assert!(require_idempotency_key(&blank, &request_context).is_err());
let mut spaced = HeaderMap::new();
spaced.insert("idempotency-key", HeaderValue::from_static("a b"));
assert!(require_idempotency_key(&spaced).is_err());
assert!(require_idempotency_key(&spaced, &request_context).is_err());
}
/// 两个路由必须真的挂在应用路由上并走统一鉴权:路径写错或漏 merge 会退化成 404。