refactor(前端): 接口错误改成 ApiError 类,catch 不再写 any
拦截器原来 reject 一个裸对象 `{ error, data }`,没有类型也没有类型守卫,于是 46 处
`catch (err: any)` 直接读 `err.data` / `err.error` —— 代码 bug 抛出的 TypeError 也按
接口错误读,弹出来的是 undefined 或英文。
- utils/api.ts:reject 改成 `ApiError extends Error`,字段名沿用 error(错误码)/
data(文案),存量读法语义不变;加 isApiError / errorCode / errorMessage。
errorMessage 对非接口错误一律用 fallback,不把 TypeError 的英文甩给学生
- 46 处 catch 全部换掉 any(45 处接口错误 + pythonSyntaxCheck 里 Skulpt 的错误对象)
- 断网等没有响应体的情况,文案从英文 "Request failed" 换成「网络异常,请检查网络后重试」
浏览器实跑核过:登录密码错误仍显示「用户名或密码不正确」(错误码分支)、
接口错误 reject 出来是 ApiError、网络不通时拿到中文文案、非接口错误走 fallback。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6406794971
commit
7a390323fe
29 files changed
+167
-106
No files matched your search
@@ -6,13 +6,45 @@ import storage from "./storage"
|
||||
|
||||
const { message: toast } = createDiscreteApi(["message"])
|
||||
|
||||
interface ApiError {
|
||||
/** 后端失败响应体的形状,见 apps/api/src/http.ts 的 failure */
|
||||
interface ErrorPayload {
|
||||
error?: {
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截器 reject 出来的就是它:`error` 是错误码,`data` 是后端文案。
|
||||
*
|
||||
* 字段名沿用原来那个裸对象 `{ error, data }`,存量调用点读 `err.error` / `err.data`
|
||||
* 的语义不变;做成 Error 的子类是为了在 catch 里能和代码 bug(TypeError 之类)分开,
|
||||
* 也顺带有了堆栈。catch 块里别写 `err: any`,用下面的 errorCode / errorMessage。
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
readonly error: string,
|
||||
readonly data: string,
|
||||
) {
|
||||
super(data)
|
||||
this.name = "ApiError"
|
||||
}
|
||||
}
|
||||
|
||||
export function isApiError(err: unknown): err is ApiError {
|
||||
return err instanceof ApiError
|
||||
}
|
||||
|
||||
/** 接口错误的错误码;不是接口错误(代码 bug、被中止……)就是 undefined */
|
||||
export function errorCode(err: unknown) {
|
||||
return isApiError(err) ? err.error : undefined
|
||||
}
|
||||
|
||||
/** 给用户看的文案:接口错误用后端文案,其它一律 fallback —— 别把 TypeError 的英文甩给学生 */
|
||||
export function errorMessage(err: unknown, fallback = "未知错误") {
|
||||
return isApiError(err) && err.data ? err.data : fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功时直接拿到业务数据本身。后端成功响应是 `{ data }`(见 apps/api/src/http.ts
|
||||
* 的 success),拦截器把 axios 外层和这层信封一起剥掉。
|
||||
@@ -74,12 +106,13 @@ instance.interceptors.response.use(
|
||||
response: AxiosResponse,
|
||||
) => AxiosResponse,
|
||||
(error) => {
|
||||
const payload = error.response?.data as ApiError | undefined
|
||||
const payload = error.response?.data as ErrorPayload | undefined
|
||||
const code = payload?.error?.code ?? "network-error"
|
||||
const message = payload?.error?.message ?? "Request failed"
|
||||
// 没有响应体 = 请求根本没到后端(断网、反代挂了、超时),学生看到的就是这一句
|
||||
const message = payload?.error?.message ?? "网络异常,请检查网络后重试"
|
||||
|
||||
handleGlobalApiError(code, message)
|
||||
return Promise.reject({ error: code, data: message })
|
||||
return Promise.reject(new ApiError(code, message))
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user