From 0a9abc25028e2613a47459d1d7511ff531e611f1 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 16 Jun 2026 18:34:55 +0800 Subject: [PATCH] fix(auth): stop parsing logout response body (avoids ZodError on null data) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: backend `/auth/logout` returns `{ success: true, data: null }` (no business payload for a fire-and-forget endpoint). The previous `AuthApi.logout` ran `unwrap(env)` (which passes null through, since null is defined) and then `LogoutResponse.fromJson(null)`, which Zod-parses as `z.object(...)` and throws "Invalid input: expected object, received null". The error was caught in `AuthRepository.logout` with `console.warn(... clearing local anyway)` — logout actually still worked (local clear ran), but a noisy red ZodError hit the console on every logout click. Fix: align `AuthApi.logout` with the existing fire-and-forget pattern used by `register` and `sendCode` — call `httpClient` and ignore the response body. Drop the now-unused `LogoutResponse` import. `AuthRepository.logout` already discards the return value, so the `Promise → Promise` signature change is safe. Bundled in this commit (unrelated): - chat-machine.actors.ts / user-machine.ts: removed stale design-doc comment blocks (IDE/linter cleanup) - user-machine.actors.ts: removed redundant `userStorage.clearUserData()` from `userLogoutActor` — `authRepo.logout` already clears local user data, so this was a no-op duplicate. - sidebar-screen.tsx: removed one stale comment line. --- src/app/sidebar/components/sidebar-screen.tsx | 1 - src/data/services/api/auth_api.ts | 14 +++++++++----- src/stores/chat/chat-machine.actors.ts | 13 ------------- src/stores/user/user-machine.actors.ts | 1 - src/stores/user/user-machine.ts | 14 -------------- 5 files changed, 9 insertions(+), 34 deletions(-) diff --git a/src/app/sidebar/components/sidebar-screen.tsx b/src/app/sidebar/components/sidebar-screen.tsx index 5f67b668..6af18836 100644 --- a/src/app/sidebar/components/sidebar-screen.tsx +++ b/src/app/sidebar/components/sidebar-screen.tsx @@ -44,7 +44,6 @@ export function SidebarScreen() { // 等价 Dart: BlocConsumer 的 listener(profile_view.dart:24-34) // 仅在 currentUser 由非 null 跳到 null 时触发清理 - // 注:chat 机器不再需要 `ChatAuthStatusChanged` 通知(已删)—— chat-screen // 通过 `useAuthState()` 主动订阅 loginStatus;这里只需重置 auth + 跳 /chat const prevUserRef = useRef(user.currentUser); useEffect(() => { diff --git a/src/data/services/api/auth_api.ts b/src/data/services/api/auth_api.ts index 84562d57..ce618471 100644 --- a/src/data/services/api/auth_api.ts +++ b/src/data/services/api/auth_api.ts @@ -17,7 +17,6 @@ import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request"; import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response"; import { LoginRequest } from "@/data/dto/auth/login_request"; import { LoginResponse } from "@/data/dto/auth/login_response"; -import { LogoutResponse } from "@/data/dto/auth/logout_response"; import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request"; import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response"; import { RegisterRequest } from "@/data/dto/auth/register_request"; @@ -102,13 +101,18 @@ export class AuthApi { } /** - * 退出登录 + * 退出登录(fire-and-forget) + * + * 后端 logout 端点的 envelope `data` 字段常为 `null`(登出端点没有业务数据), + * 不要再用 `unwrap` + `LogoutResponse.fromJson` 解析 —— Zod schema 是 z.object, + * 对 null 会抛 ZodError "Invalid input: expected object, received null"。 + * + * 对齐 `register` / `sendCode` 的写法:调通就完事,不解析响应体。 */ - async logout(): Promise { - const env = await httpClient>(ApiPath.logout, { + async logout(): Promise { + await httpClient>(ApiPath.logout, { method: "POST", }); - return LogoutResponse.fromJson(unwrap(env) as Record); } /** diff --git a/src/stores/chat/chat-machine.actors.ts b/src/stores/chat/chat-machine.actors.ts index d5c7bbb7..b64286c7 100644 --- a/src/stores/chat/chat-machine.actors.ts +++ b/src/stores/chat/chat-machine.actors.ts @@ -1,18 +1,5 @@ /** * Chat 状态机:4 个 XState actor - * - * 从 `chat-machine.ts` 抽出的"Actors"段: - * - `loadQuotaActor`(fromPromise,仅游客有意义 —— 拉日配 + 总配) - * - `loadHistoryActor`(fromPromise —— local → network → save network to local 3 步) - * - `sendMessageHttpActor`(fromPromise,已 wire —— "一次发送 = 一次返回") - * - `loadMoreHistoryActor`(fromPromise —— 翻页,不走 local-first) - * - `chatWebSocketActor`(fromCallback,长生命周期 —— 仅 userSession 期间 invoke) - * - * 设计: - * - 依赖 `chat-machine.helpers.ts`(纯函数 + 数据加载) - * - 依赖 `createChatWebSocket`(`@/core/net/chat-websocket`) - * - 保留全链日志(用户要求"不要将其删除") - * - 命名约定:`[chat-machine]` 前缀 —— 与 `chat-machine.ts` 的日志对齐 */ import { fromPromise, fromCallback } from "xstate"; diff --git a/src/stores/user/user-machine.actors.ts b/src/stores/user/user-machine.actors.ts index 2ae19b6f..020a5bbb 100644 --- a/src/stores/user/user-machine.actors.ts +++ b/src/stores/user/user-machine.actors.ts @@ -70,5 +70,4 @@ export const userFetchActor = fromPromise(async () => { */ export const userLogoutActor = fromPromise(async () => { await authRepo.logout(); - await userStorage.clearUserData(); }); \ No newline at end of file diff --git a/src/stores/user/user-machine.ts b/src/stores/user/user-machine.ts index 4e33e8a8..f36a3a73 100644 --- a/src/stores/user/user-machine.ts +++ b/src/stores/user/user-machine.ts @@ -1,19 +1,5 @@ /** * User 状态机(XState v5) - * - * 原始 Dart: lib/ui/user/bloc/user bloc + user state + user event - * - * 设计要点: - * - XState v5 `setup({...}).createMachine({...})` 声明式 API - * - HTTP 操作用 `fromPromise` actor(一次性 Promise)→ 见 `user-machine.actors.ts` - * - 纯函数 / 数据加载 → 见 `user-machine.helpers.ts` - * - 同步状态更新用 `assign` actions(inline 在 setup() 中) - * - 保持事件类型名与原 Dart 一致 - * - * 分层: - * - helpers: 无 XState 依赖(DTO 映射 + 数据加载) - * - actors: 依赖 helpers(异步 actor 实现) - * - machine: 依赖 actors(XState setup + 状态图) */ import { setup, assign } from "xstate";