Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,19 @@ const TRANSIENT_RETRY_STATUSES: &[u16] = &[408, 500, 502, 503, 504];
/// 再看一眼有意义;413 没有任何会变的量。
const PAYLOAD_SIZE_REJECTION_STATUSES: &[u16] = &[413];

/// 按**请求的路由 / 模型名**拒收的状态码:上游在解析请求时就没找到目标
/// (模型不在这一家的套餐里、路由不存在),结论只由「这份 body 长什么样」决定,
/// 同一账号重发同一份 body 必然得到同一个 404 ⇒ 不进原地重发,
/// 换账号才是有意义的动作(别的账号可能有这个模型)。
///
/// 实测形状(issue #255):OfficeAce 对目录里有、套餐里没有的名字回
/// `404 {"code":null,"message":"Invalid model."}`,旧判据把它当普通 Fatal,
/// 原地重发 3 次 × 5 秒才换号——客户端白等 15 秒,换来的还是同一个 404。
///
/// 与 [`PAYLOAD_SIZE_REJECTION_STATUSES`] 分成两个常量而不是合一:
/// 一个是「字节」,一个是「名字」,出处与措辞不同,将来各自的排除面也可能变。
const DETERMINISTIC_NOT_FOUND_STATUSES: &[u16] = &[404];

/// 这次的失败状态码是否命中「指定错误码直接换号」名单(设置页「通用 → 请求重试」)。
///
/// 命中的失败**跳过本账号**:不进 [`send_with_retry`] 的原地重发,也不做同
Expand Down Expand Up @@ -345,7 +358,10 @@ fn fallback_retry_advice(
| UpstreamErrorClass::ContentBlocked { .. } => None,
// 体积被入口拒收:重发同一份字节只会得到同一个 413(换号也一样,但那是
// 编排层的事,这里只撤「原地再发一次」这一件事)
UpstreamErrorClass::Fatal { .. } if PAYLOAD_SIZE_REJECTION_STATUSES.contains(&status) => {
UpstreamErrorClass::Fatal { .. }
if PAYLOAD_SIZE_REJECTION_STATUSES.contains(&status)
|| DETERMINISTIC_NOT_FOUND_STATUSES.contains(&status) =>
{
None
}
UpstreamErrorClass::Fatal { .. } => Some(RetryAdvice {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,10 +438,11 @@ fn a_payload_size_rejection_gets_no_in_place_retry() {
}

/// 反对照:其它 4xx/未知失败照旧走兜底原地重发(这条改动只放开"体积拒收"那一格,
/// 没有把 400 这类"含义由上游决定"的码一并排除)。
/// 没有把 400 这类"含义由上游决定"的码一并排除)。404 已于 issue #255 单独豁免
/// (路由 / 模型名类确定性失败,见 `DETERMINISTIC_NOT_FOUND_STATUSES`),不在此列。
#[test]
fn other_fatal_statuses_still_get_the_fallback_resend() {
for status in [400u16, 404, 409] {
for status in [400u16, 409] {
assert!(
fallback_retry_advice(&fatal(status), 3, status).is_some(),
"HTTP {status} 的语义由上游决定,兜底原地重发的既有行为不该变"
Expand Down Expand Up @@ -484,3 +485,29 @@ async fn a_real_413_response_is_sent_once_and_handed_back() {
"状态码 413 要原样带回,不该被折成别的失败"
);
}

#[test]
fn a_deterministic_404_skips_the_in_place_resend() {
// issue #255 的形状:OfficeAce 对目录里有、套餐里没有的名字回
// `404 {"code":null,"message":"Invalid model."}`,旧判据把它当普通 Fatal,
// 原地重发 3 次 × 5 秒才换号——客户端白等 15 秒,换来的还是同一个 404。
let not_found = UpstreamErrorClass::Fatal {
status: 404,
upstream_code: None,
message: "上游返回 404: {\"code\":null,\"message\":\"Invalid model.\"}".to_string(),
};
assert!(
fallback_retry_advice(&not_found, 3, 404).is_none(),
"名字类确定性失败不该再原地重发"
);
// 对照:普通 400 仍走原地重发(400 的语义由上游定,重发是安全默认)
let bad_request = UpstreamErrorClass::Fatal {
status: 400,
upstream_code: None,
message: "上游返回 400: whatever".to_string(),
};
assert!(
fallback_retry_advice(&bad_request, 3, 400).is_some(),
"404 的豁免不许顺手把 400 也豁免了"
);
}
Loading