前端HTTP状态码的处理
前端如何统一处理 HTTP 状态码、网络错误与业务 code(以 Axios 拦截器为例)。
#type / howto
#status / growing
#tech / network
#platform / browser
#resource / http
前端HTTP状态码的处理
[!info] related notes
- 所属 MOC: http-and-frontend-networking-moc
- 状态码语义(参考表): http-status-codes
- 请求选型: frontend-http-requests-xhr-fetch-axios
- Axios 相关: Axios 响应结构, Axios 错误模型, Axios 拦截器, Axios 实例
下面给出一套可落地的“前端请求统一处理”做法:
- 不重新解释所有状态码(语义查表请看:http-status-codes)。
- 重点讲工程上怎么把 HTTP status、网络错误、业务 code 统一成一个稳定的错误模型。
- 以 Axios 拦截器为例;如果你用 Fetch/XHR,也可以复用同样的分层思路。
要点(工程口径)
- 分层:HTTP status / 网络错误 / 业务 code 是三件事,分别处理,最后再统一成一个“前端错误对象”。
- 收口:所有请求都走同一个 client(Axios instance 或你的 fetch wrapper),不要在业务页面里散落
try/catch + if/else。 - 策略:
- 网络/超时/DNS/CORS 等(没有 response) -> 统一提示 + 上报 + 可选重试。
- HTTP 401(认证失效) -> 尝试刷新 token(队列化等待),刷新失败才跳登录。
- HTTP 403(鉴权失败) -> 直接提示无权限,不要进入刷新 token 的流程。
- HTTP 429/503(限流/过载) -> 建议指数退避重试(只对幂等请求做)。
- 2xx 但业务 code 失败 -> 统一提示,但保留原始信息用于日志/排障。
- 文件下载:尽量返回原始 response(或 blob),不要强行走“业务 code”解析。
最小状态码决策表(只给工程上常用的)
| 情况 | 你在前端应该做什么 |
|---|---|
无 response(网络错误/超时) | 提示“网络异常”,可上报;必要时重试 |
401 | 触发 refresh token;刷新失败再去登录 |
403 | 提示无权限;不要刷新 token |
404 | 资源不存在;可按业务提示(路由/接口/静态资源) |
409 | 冲突(并发写/重复提交);提示用户采取下一步 |
422 | 参数/校验失败;把字段错误映射到表单 |
429 | 限流;指数退避重试(幂等请求) |
5xx | 服务端异常;提示稍后再试 + 上报 |
示例:Axios instance + 拦截器(可当模板)
建议把它放在一个单独模块(如 service.ts / httpClient.ts),业务代码只调用封装后的 request() 或 get/post。
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { config } from './config'
// NOTE: 下面的 cache/message/router 只是示意,按你的项目替换
import { useCache } from '@/utils/cache'
import { message } from '@/utils/message'
import router from '@/router'
const { wsCache } = useCache()
const instance: AxiosInstance = axios.create({
baseURL: config.base_url,
timeout: config.request_timeout,
headers: { 'Content-Type': String(config.default_headers) }
})
// 请求拦截器:注入 token / lang / 通用 headers
instance.interceptors.request.use((req: AxiosRequestConfig) => {
const token = wsCache.get('TOKEN')
if (token) req.headers = { ...req.headers, Authorization: `Bearer ${token}` }
const lang = wsCache.get('LANG')
if (lang) req.headers = { ...req.headers, Lang: lang }
return req
}, (error) => {
return Promise.reject(error)
})
// 刷新 token 的简单队列实现(示意)
let isRefreshing = false
let requestsQueue: Array<(token: string | null) => void> = []
const pushQueue = (cb: (token: string | null) => void) => requestsQueue.push(cb)
const runQueue = (token: string | null) => {
requestsQueue.forEach(cb => cb(token))
requestsQueue = []
}
// 响应拦截器:统一处理 HTTP + 业务 code
instance.interceptors.response.use(
(response: AxiosResponse) => {
// 文件下载直接返回原始 response
if (response.config.responseType === 'blob' || response.headers['content-type']?.includes('application/octet-stream')) {
return response
}
const res = response.data
// 统一判定业务成功(以 config.result_code 为准)
if (res && res.code !== undefined) {
if (res.code === config.result_code) {
return res.data === undefined ? res : res // 视项目约定可直接返回 res.data
}
// 业务错误:统一提示并抛出
message.error(res.message || '操作失败')
return Promise.reject({ isBusinessError: true, ...res })
}
// 无业务 code,直接返回 response.data
return res
},
async (error) => {
const { response, config: reqConfig } = error || {}
// 网络/超时
if (!response) {
message.error('网络错误或请求超时')
return Promise.reject(error)
}
const status = response.status
if (status === 401) {
// 刷新 token 流程
if (!isRefreshing) {
isRefreshing = true
try {
// 假设 refreshToken 接口在 /auth/refresh,返回新 token
const refreshRes = await axios.post(`${config.base_url}/auth/refresh`, { /*...*/ })
const newToken = refreshRes.data?.token
wsCache.set('TOKEN', newToken)
runQueue(newToken)
} catch (e) {
runQueue(null)
wsCache.remove('TOKEN')
router.push({ name: 'Login' })
} finally {
isRefreshing = false
}
}
return new Promise((resolve, reject) => {
pushQueue((token) => {
if (!token) {
reject(error)
return
}
// 重试原请求
reqConfig.headers = { ...reqConfig.headers, Authorization: `Bearer ${token}` }
resolve(axios(reqConfig))
})
})
}
if (status === 403) {
message.error('无权限访问')
return Promise.reject(error)
}
if (status >= 500) {
message.error('服务端异常,请稍后重试')
} else if (status >= 400) {
const msg = response.data?.message || `请求错误 ${status}`
message.error(msg)
}
return Promise.reject(error)
}
)
export default instance
如何在你的项目里落地(简短)
- 让所有请求都走这一个
instance。 - 业务代码只处理“成功结果”,不要再到处写
if (status === 401)。 - 需要下载/流式时,提供单独方法或开关,绕过“业务 code 解包”。
一句话结论
统一 client + 清晰分层(网络/HTTP/业务)+ 可控的 token 刷新与重试策略,是前端 HTTP 错误处理的关键。