读body失败保留已确认状态码

- auth_session:拿到 status 后 response.text() 失败不再一律报网络不可达;非 2xx 按状态码走既有分类(serverMessage 为 None,如 503 → authServiceUnavailable),只有 2xx 响应没收完才归传输层故障
- 抽出 classify_unreadable_body 并补单测(503 / 会话 401 / 400 / 2xx)
- 决策记录追加该口径
This commit is contained in:
2026-10-02 13:47:03 +08:00
parent 836f8a7348
commit a512786996
2 changed files with 45 additions and 6 deletions
@@ -573,6 +573,20 @@ fn map_auth_failure(status: StatusCode, body: &str, route: AuthRoute) -> ClientA
})
}
/// body 读取失败时的归类。
///
/// 状态码此时已经拿到:非 2xx 按状态码走已有分类(body 没读到,服务端原文就是 `None`),
/// 不把服务端失败误报成传输层故障;只有 2xx 响应没收完才归网络不可达。
fn classify_unreadable_body(status: StatusCode, route: AuthRoute) -> ClientAuthError {
if status.is_success() {
ClientAuthError::AuthNetworkFailure(AuthNetworkFailure {
reason: AuthNetworkReason::Unreachable,
})
} else {
map_auth_failure(status, "", route)
}
}
fn response_data(body: &str) -> Result<Value, ClientAuthError> {
let value: Value = serde_json::from_str(body)
.map_err(|_| response_invalid(AuthResponseInvalidReason::NotJson))?;
@@ -656,12 +670,10 @@ async fn request_auth(
CookiePolicy::Capture | CookiePolicy::Require => refresh_cookie_from_response(&response),
CookiePolicy::Ignore => None,
};
// 拿到状态码后读 body 失败属于传输层故障:按网络不可达上报,不猜契约。
let text = response.text().await.map_err(|_| {
ClientAuthError::AuthNetworkFailure(AuthNetworkFailure {
reason: AuthNetworkReason::Unreachable,
})
})?;
let text = match response.text().await {
Ok(text) => text,
Err(_) => return Err(classify_unreadable_body(status, auth_route(route))),
};
if !status.is_success() {
return Err(map_auth_failure(status, &text, auth_route(route)));
}
@@ -1407,6 +1419,32 @@ mod tests {
);
}
/// 读 body 失败时状态码已经拿到,不能被压成「网络不可达」。
#[test]
fn unreadable_body_keeps_the_known_status() {
assert_eq!(
classify_unreadable_body(StatusCode::SERVICE_UNAVAILABLE, AuthRoute::PhoneLogin),
ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable { status: 503 })
);
assert_eq!(
classify_unreadable_body(StatusCode::UNAUTHORIZED, AuthRoute::Session),
ClientAuthError::SessionInvalidated
);
assert_eq!(
classify_unreadable_body(StatusCode::BAD_REQUEST, AuthRoute::PasswordEntry),
ClientAuthError::PasswordLoginRejected(PasswordLoginRejected {
server_message: None
})
);
// 只有 2xx 响应没收完才是传输层故障。
assert_eq!(
classify_unreadable_body(StatusCode::OK, AuthRoute::Other),
ClientAuthError::AuthNetworkFailure(AuthNetworkFailure {
reason: AuthNetworkReason::Unreachable,
})
);
}
#[test]
fn keepalive_only_refreshes_after_the_freshness_window() {
let issued = 1_000_000_u64;
@@ -13,6 +13,7 @@
- 决策(2026-10-01,Rust 侧不再降级):`refresh_session_inner` 的非权威失败直接 `Err(ClientAuthError)`,`ClientAuthStateView` / `ClientAuthRefreshView` 删除 `errorMessage`,续期结果删除 `failed`;`ClientAuthState` 收敛为 `authenticated | unauthenticated`,`ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。
- 追加(2026-10-02,429 按路由判定):`/api/auth/phone/login` 验证码错误次数耗尽返回的 429 是用户可修正的输入问题,映射为 `phoneCodeLoginRejected`(复用现有业务变体、不进错误池);发码路由仍是 `smsCodeThrottled`,其余路由的 429 仍是 `unexpectedRejection`。
- 追加(2026-10-02,401 归 phoneCodeLoginRejected):`/api/auth/phone/login` 的 401 只来自「用户不存在」(验证码错误/失效/过期在服务端都是 400,已由 `phoneCodeLoginRejected { serverMessage }` 带原文);顶层变体 `smsCodeRejected` 退役删除,前端三个 catch 去掉了它那个「验证码错误或已过期」的固定分支,`phoneCodeLoginRejected` 的文案统一为「验证码登录失败:<服务端原文>」。
- 追加(2026-10-02,读 body 失败按已确认状态码归类):AGC 认证请求拿到 `status` 后 `response.text()` 失败,不再一律压成 `authNetworkFailure { unreachable }`;非 2xx 走既有分类(`serverMessage` 为 `None`,如 503 → `authServiceUnavailable { 503 }`),只有 2xx 响应没收完才算传输层故障。分类收敛在 `classify_unreadable_body`。
- 追加(2026-10-02,系统类失败保留原始错误载荷):`clientSessionPersistFailed` / `runtimeSessionInstallFailed` / `authClientInitFailed` 都带 `detail: string`(原始 error),既让调用方有机会分流处理,也让报告包带够诊断信息;原始 error 同时经 `app_log!`(落盘前过 `sanitize_diagnostic_message`)记一行本地日志。`detail` 不贴到界面上:三个 catch 用本操作的固定文案(登录检查 / 发码 / 登录各自不同)。取代上一版"本机 IO 失败不进载荷、原始 error 只进日志"。
- 影响范围(2026-10-01 第二轮):`apps/ai-game-creator-shell/src/services/{clientAuth.ts,platformSession.ts}`(`clientAuthError.ts` 删除)、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、对应 vitest 用例。