创见博客
前端常见 HTTP 状态码:从 200、304 到 502、504
七崽爱吃小饼干2026/08/25阅读 0

打开一个前端页面,在 Network 面板里经常会看到这样的结果:

text
GET  /api/profile      200
POST /api/orders       201
GET  /assets/app.js    304
GET  /api/profile      401
POST /api/comments     422
GET  /api/recommend    502

这些三位数字不是简单的“成功或失败”。它们告诉客户端:请求处理到了哪一步、下一步应该做什么,以及问题更可能出在浏览器、业务参数、权限还是上游服务。

前端不需要背下所有 HTTP 状态码,但应该理解常见状态码的语义,并把它们转换成正确的页面行为。本文从实际开发场景出发,整理最常见的状态码、容易混淆的区别,以及 Fetch 和 Axios 中的处理方式。

先看状态码的五个类别

HTTP 状态码的第一位数字表示大类:

范围含义前端常见关注点
1xx请求正在处理协议升级、上传前确认
2xx请求成功展示数据、更新页面状态
3xx重定向或需要客户端继续处理页面跳转、缓存复用
4xx客户端侧请求存在问题参数、登录、权限、限流
5xx服务端处理失败服务异常、网关错误、超时

“客户端错误”不等于一定是前端代码写错。例如登录过期返回 401,用户没有某项业务权限返回 403,都属于正常业务分支。类似地,5xx 也不一定是当前业务服务崩溃,还可能是网关无法连接上游服务。

2xx:请求已经成功

200 OK:最常见的成功响应

查询列表、获取详情、提交普通操作后,服务端通常返回 200:

http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 42,
  "nickname": "Visionary"
}

前端收到 200 后解析响应内容并更新页面:

ts
const response = await fetch('/api/profile');

if (!response.ok) {
  throw new Error(`Request failed: ${response.status}`);
}

const profile = await response.json();

response.ok 在状态码为 200 到 299 时是 true,因此它比只判断 response.status === 200 更适合通用请求封装。

201 Created:资源创建成功

创建文章、订单或评论时,服务端可以返回 201:

http
HTTP/1.1 201 Created
Location: /api/articles/456
Content-Type: application/json

{ "id": 456 }

201 不只是“请求成功”,还明确表示新资源已经创建。服务端可以通过 Location 响应头给出新资源地址,也可以在响应体中返回 ID。

前端常见动作是跳转到详情页,或把新记录插入当前列表:

ts
const response = await fetch('/api/articles', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(form),
});

if (response.status === 201) {
  const article = await response.json();
  router.push(`/articles/${article.id}`);
}

202 Accepted:请求已接收,但还没处理完

视频转码、批量导出、AI 内容生成等任务可能无法在一次 HTTP 请求内完成。服务端接收任务后返回 202,表示任务已经进入处理流程,但最终结果尚未产生。

json
{
  "taskId": "task_123",
  "status": "processing"
}

此时前端不能把 202 当作“任务已经完成”,而应该轮询任务状态、订阅 WebSocket/SSE 消息,或者展示“处理中”:

ts
const response = await fetch('/api/exports', { method: 'POST' });
const { taskId } = await response.json();

startPolling(`/api/tasks/${taskId}`);

204 No Content:成功,但没有响应体

删除资源、取消收藏或只更新状态时,服务端可能返回 204。它表示操作成功,但响应体为空。

ts
const response = await fetch(`/api/todos/${id}`, {
  method: 'DELETE',
});

if (response.status === 204) {
  removeTodoFromPage(id);
}

不要对 204 直接调用 response.json(),否则会因为空响应体产生 JSON 解析错误。通用请求函数需要先检查状态码或响应体是否为空。

3xx:重定向与缓存

301 和 308:永久重定向

网站更换域名、页面永久迁移或规范化 URL 时,服务端会使用永久重定向:

http
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page

301 和 308 都表示资源永久迁移。重要区别是:308 明确要求重定向后保留原请求方法和请求体;历史客户端处理 301 时,可能把 POST 改成 GET。

对普通页面导航,浏览器通常会自动跟随重定向。前端在 Network 面板中需要打开请求链,才能看到最初的 301/308 和最终响应。

302、303 和 307:临时重定向

这几个状态码都表示当前资源临时指向其他地址,但方法处理不同:

状态码常见语义重定向后的请求方法
302 Found临时跳转历史实现可能改为 GET
303 See Other到另一个地址查看结果使用 GET
307 Temporary Redirect临时跳转并保留请求语义保留原方法和请求体

例如提交表单后,服务端可以返回 303,让浏览器通过 GET 打开结果页,避免用户刷新页面时重复提交表单。

需要注意,fetch 默认会自动跟随重定向。前端代码最后看到的经常是目标地址返回的 200,而不是中间的 302。可以通过 response.redirected 和 response.url 判断是否发生过跳转:

ts
const response = await fetch('/api/entry');

if (response.redirected) {
  console.log('Final URL:', response.url);
}

304 Not Modified:资源没变,继续使用缓存

304 属于 3xx,但它不是跳到另一个 URL。它用于 HTTP 协商缓存:客户端已经有一份资源,向服务器询问这份缓存是否还能使用。

请求可能携带缓存标识:

http
GET /assets/app.js HTTP/1.1
If-None-Match: "a1b2c3"

如果资源没有变化,服务器返回:

http
HTTP/1.1 304 Not Modified
ETag: "a1b2c3"

响应体为空,浏览器继续使用本地缓存中的 app.js。因此 304 的实际流程是:

text
浏览器携带缓存标识发起请求
  -> 服务器确认资源未变化
  -> 返回 304,不重复传输内容
  -> 浏览器读取已有缓存

它被归入 3xx,是因为当前响应本身不提供最终内容,客户端还要使用已有缓存完成资源读取。它不是错误,也不是传统的 URL 重定向。

前端在 DevTools 中看到 304 时,一般不需要修改业务代码。应该检查的是 Cache-Control、ETag、Last-Modified 和 CDN 缓存配置是否符合预期。

4xx:请求、身份或权限存在问题

400 Bad Request:请求格式或参数有误

JSON 格式错误、缺少必要字段、参数类型不正确,都可能返回 400:

json
{
  "code": "INVALID_REQUEST",
  "message": "title is required"
}

前端应该保留服务端提供的稳定错误码,用它决定页面行为,而不是根据自然语言文案做判断:

ts
if (error.code === 'INVALID_REQUEST') {
  showToast(error.message);
}

401 Unauthorized:尚未通过身份认证

401 的名字容易让人误解。它主要表示请求没有有效的身份凭证,例如:

  • 用户尚未登录;
  • Access Token 已过期;
  • Cookie 没有携带;
  • Token 格式错误或签名无效。

前端常见处理是尝试刷新 Token,刷新失败后清理登录状态并跳转登录页:

ts
if (response.status === 401) {
  const refreshed = await refreshAccessToken();

  if (!refreshed) {
    clearSession();
    location.assign(`/login?redirect=${encodeURIComponent(location.href)}`);
  }
}

请求拦截器需要防止多个并发 401 同时触发多次刷新和重复跳转。通常会用一个共享中的刷新 Promise,让其他失败请求等待刷新结果后再重试一次。

403 Forbidden:身份有效,但没有权限

403 表示服务端理解请求,也知道当前用户是谁,但拒绝执行。例如普通成员访问管理员页面,或作者尝试修改不属于自己的文章。

text
401:你还没有证明自己是谁
403:已经知道你是谁,但你不能做这件事

前端遇到 403 时不应无限刷新 Token。更合理的行为是展示无权限页面、禁用对应操作,或者引导用户申请权限。

按钮置灰只能改善体验,不能代替服务端鉴权。用户仍然可以绕过页面,直接构造 HTTP 请求。

404 Not Found:目标资源不存在

常见场景包括路由不存在、文章已删除、请求 ID 错误,或接口地址拼写错误。

详情页应该区分“加载中”“请求失败”和“资源不存在”:

tsx
if (status === 404) {
  return <NotFound description="文章不存在或已被删除" />;
}

有些系统为了避免泄露私有资源是否存在,会对无权访问的资源也返回 404,而不是 403。这是安全策略,不一定是接口实现错误。

405 Method Not Allowed:请求方法不受支持

接口只支持 GET,前端却发送了 POST,可能得到 405。响应通常可以通过 Allow 头声明支持的方法:

http
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD

排查时检查请求方法、接口文档、路由处理函数和代理重写配置。405 与 404 的区别是:资源路径可能存在,只是当前方法不被允许。

409 Conflict:请求与当前资源状态冲突

409 常见于:

  • 注册时用户名已经存在;
  • 使用旧版本数据覆盖已被别人更新的记录;
  • 重复创建具有唯一约束的资源;
  • 订单当前状态不允许取消。

例如多人编辑可以通过版本号避免静默覆盖:

http
PUT /api/documents/42
If-Match: "version-7"

如果服务器上的文档已经变成新版本,可以返回 409 或更具体的 412 Precondition Failed。前端应提示用户重新加载、比较差异或确认是否覆盖,而不是简单显示“系统错误”。

413 Content Too Large:上传内容过大

上传图片、视频或导入文件时,网关和应用服务都可能限制请求体大小。超过限制后会返回 413。

前端可以在选择文件时提前校验:

ts
const MAX_SIZE = 10 * 1024 * 1024;

if (file.size > MAX_SIZE) {
  showToast('文件不能超过 10 MB');
  return;
}

客户端校验只用于尽早反馈,服务端仍然必须校验。若文件在到达应用服务前就被 Nginx 或 CDN 拦截,还需要调整对应网关限制。

415 Unsupported Media Type:请求内容类型不支持

接口要求 JSON,但请求没有正确设置 Content-Type,可能返回 415:

ts
await fetch('/api/articles', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(article),
});

上传 FormData 时则不要手动拼接 multipart/form-data 的 boundary,让浏览器自动生成完整的 Content-Type。

422 Unprocessable Content:格式可解析,但业务校验失败

请求是合法 JSON,字段也能读取,但邮箱格式不正确、标题太长或结束时间早于开始时间,可以返回 422。

json
{
  "code": "VALIDATION_FAILED",
  "fields": {
    "email": "邮箱格式不正确",
    "title": "标题不能超过 100 个字符"
  }
}

前端可以把错误精确展示到表单字段旁:

ts
if (response.status === 422) {
  const result = await response.json();
  form.setErrors(result.fields);
}

实践中 400 和 422 的边界并非所有团队都一致。比争论唯一标准更重要的是:团队保持一致,并提供稳定的业务错误结构。

429 Too Many Requests:请求过于频繁

验证码发送、登录尝试、搜索建议和公开 API 经常需要限流。超出限制后,服务端返回 429,并可能通过 Retry-After 告诉客户端多久后再试:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 30

前端可以显示倒计时、暂时禁用按钮,并避免立即自动重试。输入联想场景还应使用防抖和取消旧请求,减少无效流量。

5xx:服务端或上游服务处理失败

500 Internal Server Error:服务内部异常

未捕获异常、数据库错误或代码逻辑故障常被归为 500。前端通常无法自行修复,只能提供稳定的失败界面和重试入口。

错误上报时建议记录:

  • 请求 URL 和方法;
  • 状态码;
  • 后端返回的业务错误码;
  • requestId 或 traceId;
  • 当前页面和操作上下文。

不要把服务端堆栈、SQL 或内部路径直接返回给浏览器。

502 Bad Gateway:网关收到无效的上游响应

浏览器访问的可能不是业务服务本身,而是 CDN、Nginx、API Gateway 或反向代理:

text
浏览器 -> CDN / 网关 -> Node.js 服务 -> 数据服务

如果网关无法正常连接上游,或上游返回了无效响应,可能出现 502。前端开发中常见原因包括本地代理目标没有启动、代理端口配置错误,以及线上服务刚好重启。

503 Service Unavailable:服务暂时不可用

服务维护、过载、实例没有准备好,或者熔断策略生效时,可能返回 503。它通常表示“现在不能处理”,而不是接口永久不存在。

服务端可以携带 Retry-After。前端可以对幂等查询进行有限次数的退避重试,但支付、创建订单等非幂等请求不能在没有幂等保障时自动重放。

504 Gateway Timeout:网关等待上游超时

504 表示网关已经把请求交给上游,但在规定时间内没有等到响应。慢查询、下游接口卡住或超时时间配置不合理都可能触发它。

text
502:上游连接或响应无效
503:服务当前不可用
504:上游处理太久,网关等超时了

前端看到 504 时,可以提示用户稍后重试;如果请求本身可能已经在服务端执行,还要先查询最终状态,避免用户重复提交产生两笔订单或两个任务。

Fetch 和 Axios 对错误状态的处理不同

这是前端封装请求时最容易踩的坑之一。

Fetch 在收到 404、500 时,Promise 仍然会正常 fulfilled。只有网络失败、请求被中止等情况才会 reject,因此必须主动判断 response.ok:

ts
async function request<T>(url: string, init?: RequestInit): Promise<T> {
  const response = await fetch(url, init);

  if (!response.ok) {
    const error = await response.json().catch(() => null);
    throw new HttpError(response.status, error);
  }

  if (response.status === 204) {
    return undefined as T;
  }

  return response.json() as Promise<T>;
}

Axios 默认会把 2xx 之外的响应作为 rejected Promise,状态码位于 error.response.status:

ts
try {
  await axios.get('/api/profile');
} catch (error) {
  if (axios.isAxiosError(error)) {
    console.log(error.response?.status);
  }
}

不管使用哪种库,都应该区分三类失败:

text
HTTP 错误:服务器返回了 4xx 或 5xx
网络错误:断网、DNS、连接失败、请求被中止
浏览器安全限制:CORS、Mixed Content、证书问题

后两类问题可能根本没有可读取的 HTTP 状态码。

为什么有时 Network 面板显示 (failed)、CORS error 或状态码 0

HTTP 状态码由服务器响应产生。如果浏览器没有拿到一个允许页面读取的响应,业务代码就不一定能看到状态码。

常见情况包括:

  • 用户断网或 DNS 解析失败;
  • TLS 证书错误;
  • HTTPS 页面请求 HTTP 资源,被 Mixed Content 策略拦截;
  • 跨域请求未通过 CORS 检查;
  • 请求被 AbortController 主动取消;
  • 浏览器扩展或安全策略拦截请求。

例如 CORS 失败时,服务端甚至可能已经返回 200,但浏览器不允许 JavaScript 读取该响应。前端拿到的是类似 TypeError: Failed to fetch 的网络层错误,而不是可供业务判断的 200。

因此排障时要同时查看 Console、Network 请求详情、服务端日志和代理配置,不能把所有失败都归为“接口返回了错误状态码”。

一套实用的前端处理策略

可以按状态码职责设计统一请求层:

状态建议处理
200-299正常解析;单独兼容 204 空响应
304通常交给浏览器缓存机制处理
400/422展示请求或表单校验错误
401刷新凭证,失败后跳转登录
403展示无权限状态,不重复刷新凭证
404展示资源不存在或路由兜底页
409提示状态冲突,引导刷新或确认
413提示文件超限,并在选择阶段提前校验
429根据 Retry-After 限制再次操作
500-504展示服务异常;仅安全地重试幂等请求
无状态码按网络、CORS、取消请求等方向排查

页面提示也应尽量具体。相比统一弹出“网络错误”,下面的反馈更有操作价值:

text
401 -> 登录已过期,请重新登录
403 -> 你没有编辑这篇文章的权限
404 -> 文章不存在或已被删除
409 -> 内容已被其他人更新,请刷新后重试
413 -> 文件超过 10 MB,请压缩后上传
429 -> 操作过于频繁,请 30 秒后再试
504 -> 服务响应超时,请稍后重试

最后记住这些区别

不需要孤立地背数字,可以把常见状态码放进几个问题中:

text
成功了吗?
200:成功并返回内容
201:成功创建资源
202:已接收,但仍在处理
204:成功,但没有响应体

登录和权限有什么区别?
401:缺少有效身份
403:身份有效,但没有权限

请求哪里不对?
400:请求格式或参数有问题
409:与资源当前状态冲突
413:请求体太大
415:内容类型不支持
422:内容可解析,但业务校验失败
429:请求过于频繁

服务为什么失败?
500:服务内部异常
502:网关收到无效上游响应
503:服务暂时不可用
504:网关等待上游超时

状态码是 HTTP 层对处理结果的概括,业务错误码则负责表达更具体的原因。前端最可靠的做法,不是把所有非 200 请求都弹成同一句“请求失败”,而是结合状态码、业务错误码和当前操作,给用户明确且安全的下一步。

评论
0/100