前端HTTP状态码的处理

前端如何统一处理 HTTP 状态码、网络错误与业务 code(以 Axios 拦截器为例)。

#type / howto #status / growing #tech / network #platform / browser #resource / http

前端HTTP状态码的处理

[!info] related notes


下面给出一套可落地的“前端请求统一处理”做法:

  • 不重新解释所有状态码(语义查表请看: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 错误处理的关键。

创建于 2025/1/1 更新于 2026/5/27