HTTP 响应码有哪些?分别代表什么意思?8 年 Java 开发:从踩坑到实战(附 Spring Boot 代码)
刚入行那年,我写了个用户登录接口,前端反馈 “点登录没反应”。查日志发现接口返回 200,但响应体里藏着 “用户名不存在” 的错误信息 —— 前端以为 200 就是成功,没解析错误内容,用户卡了半天。后来又踩过 “把 401(未登录)返回成 403(权限不足)”“500 直接抛堆栈给用户” 的坑,才明白:HTTP 响应码不是随便填的数字,而是前后端沟通的 “通用语言”,填错了就是 “鸡同鸭讲” 。
今天就从 8 年 Java 开发的实战角度,把 HTTP 响应码的 “分类逻辑 + 业务场景 + 代码落地 + 避坑指南” 讲透 —— 不管你是写接口还是调接口,看完都能避免 “响应码乱用” 的尴尬。
一、先搞懂:HTTP 响应码为什么分 5 大类?
HTTP 响应码总共分 5 类(1xx-5xx),每类对应 “请求处理的不同阶段”,记住这个逻辑就不会乱:
类别 | 核心含义 | 场景类比 | 开发中遇到的频率 |
---|---|---|---|
1xx | 信息性响应(临时通知) | 外卖下单后 “商家已接单” | 极少(几乎不用) |
2xx | 成功响应(请求搞定了) | 外卖 “已送达” | 最高(天天用) |
3xx | 重定向(需要进一步操作) | 外卖 “地址不对,引导改地址” | 中(登录、缓存常用) |
4xx | 客户端错误(你发错了) | 外卖 “没填收货地址,下不了单” | 高(参数错、没登录) |
5xx | 服务器错误(我搞砸了) | 外卖 “商家系统崩溃,下不了单” | 中(代码 bug、服务挂了) |
简单说:2xx 是 “好事”,4xx 是 “你的错”,5xx 是 “我的错”,3xx 是 “再走一步”,1xx 是 “临时通知” 。搞懂这个分类,后面记具体响应码就轻松了。
二、每类关键响应码:业务场景 + Java 代码落地
不是所有响应码都常用,8 年开发下来,高频用到的就 10 来个。每个都附 “业务场景 + Spring Boot 代码示例”,直接复用。
1. 2xx 成功响应:“事儿办成了,这是结果”
核心:客户端请求完全符合预期,服务器成功处理。高频用 3 个:200、201、202。
(1)200 OK:通用成功(最常用)
- 业务场景:请求成功且返回数据,比如 “查询用户信息”“获取商品列表”“登录成功”。
- 注意:200 只代表 “请求处理成功”,不代表 “业务成功”—— 比如登录接口,用户名密码正确返回 200+“登录成功”,用户名不存在不能返回 200(应该返回 400 或 401)。
(2)201 Created:创建资源成功
- 业务场景:客户端触发 “创建资源” 的操作,比如 “创建订单”“新增用户”“上传文件”。
- 关键:返回 201 时,最好在响应头加
Location
,告诉客户端新资源的地址(比如新订单的详情地址)。
(3)202 Accepted:异步处理中(少用但重要)
- 业务场景:请求已接收,但需要异步处理(不能马上返回结果),比如 “提交大数据导出任务”“发起异步对账”。
- 区别 200:200 是 “已完成”,202 是 “已接收,正在做”。
Spring Boot 代码示例(返回 2xx)
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// 1. 创建订单:返回201 Created(带新资源地址)
@PostMapping
public ResponseEntity<OrderVO> createOrder(@RequestBody OrderDTO orderDTO) {
// 业务逻辑:创建订单
OrderVO orderVO = orderService.create(orderDTO);
// 设置响应头:Location指向新订单的详情接口
HttpHeaders headers = new HttpHeaders();
headers.add("Location", "/api/orders/" + orderVO.getId());
// 返回201 + 订单信息 + 响应头
return new ResponseEntity<>(orderVO, headers, HttpStatus.CREATED);
}
// 2. 查询订单:返回200 OK
@GetMapping("/{id}")
public ResponseEntity<OrderVO> getOrder(@PathVariable Long id) {
OrderVO orderVO = orderService.getById(id);
// 200可以省略,ResponseEntity默认用200
return ResponseEntity.ok(orderVO);
}
// 3. 导出订单:返回202 Accepted(异步处理)
@PostMapping("/export")
public ResponseEntity<String> exportOrders(@RequestBody ExportDTO exportDTO) {
// 业务逻辑:提交导出任务,返回任务ID
String taskId = exportService.submitTask(exportDTO);
// 返回202 + 任务ID(客户端用任务ID查结果)
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body("导出任务已接收,任务ID:" + taskId);
}
}
2. 3xx 重定向:“你得换个地方找结果”
核心:客户端请求的资源 “不在这”,需要进一步操作才能拿到结果。高频用 3 个:301、302、304。
(1)301 Moved Permanently:永久重定向
- 业务场景:资源 “永久搬家” 了,比如旧域名
xxx.com
迁到新域名new.xxx.com
,旧接口/api/v1/order
迁到/api/v2/order
。 - 注意:浏览器会缓存 301,下次直接访问新地址,所以确定 “永久迁移” 才用。
(2)302 Found:临时重定向(最常用)
- 业务场景:资源 “临时在别的地方”,比如 “未登录用户访问需要登录的页面,跳转到登录页”“秒杀活动结束,跳转到活动结束页”。
- 区别 301:302 是临时的,浏览器不缓存;301 是永久的,浏览器缓存。
(3)304 Not Modified:资源未修改(缓存专用)
- 业务场景:客户端请求缓存资源时,服务器告诉它 “资源没改,你用本地缓存就行”,比如 “静态资源(CSS/JS/ 图片)”“查询不常变的数据(比如省份列表)”。
- 原理:客户端请求时带
If-Modified-Since
(最后修改时间)或If-None-Match
(ETag 标识),服务器判断资源没改,返回 304,不返回资源内容(省带宽)。
Spring Boot 代码示例(3xx 重定向)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@RequestMapping("/api")
public class RedirectController {
// 1. 302临时重定向:未登录跳登录页
@GetMapping("/user/profile")
public RedirectView userProfile(HttpServletRequest request) {
// 判断是否登录(实际项目用Session/Token)
Boolean isLogin = (Boolean) request.getSession().getAttribute("isLogin");
if (isLogin == null || !isLogin) {
// 302重定向到登录页(默认是302,指定status=301就是永久重定向)
return new RedirectView("/api/login", true);
}
// 已登录,返回用户信息(200)
return new RedirectView("/api/user/info", true);
}
// 2. 304 Not Modified:静态资源缓存
@GetMapping("/static/provinces")
public void getProvinces(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. 获取客户端带的"If-Modified-Since"头(资源最后修改时间)
String ifModifiedSince = request.getHeader("If-Modified-Since");
// 2. 假设省份列表最后修改时间是2024-01-01(实际项目存在数据库/配置中)
String lastModified = "Mon, 01 Jan 2024 00:00:00 GMT";
// 3. 判断资源是否修改:如果客户端带的时间 >= 最后修改时间,返回304
if (ifModifiedSince != null && ifModifiedSince.equals(lastModified)) {
response.setStatus(HttpStatus.NOT_MODIFIED.value());
return;
}
// 4. 资源已修改,返回200 + 数据 + 最后修改时间头
response.setStatus(HttpStatus.OK.value());
response.setHeader("Last-Modified", lastModified);
response.getWriter().write("["北京","上海","广州"]");
}
}
3. 4xx 客户端错误:“你发的请求有问题,我没法处理”
核心:问题出在客户端(比如参数错、没登录、要的资源不存在),服务器没义务处理错误请求。高频用 5 个:400、401、403、404、429。
(1)400 Bad Request:请求参数错误(最常用)
- 业务场景:客户端提交的参数不符合要求,比如 “下单接口没传商品 ID”“手机号格式不对”“年龄传了字符串”。
- 避坑:不要把 400 和 404 搞混 ——400 是 “参数错”,404 是 “路径错 / 资源不存在”。
(2)401 Unauthorized:未登录 / 身份未验证
- 业务场景:客户端访问需要登录的接口,但没传登录凭证(Token/Session),比如 “未登录用户调用下单接口”“Token 过期”。
- 关键:返回 401 时,最好在响应头加
WWW-Authenticate
,告诉客户端 “需要怎么登录”(比如 “请传 Bearer Token”)。
(3)403 Forbidden:权限不足(已登录但没权限)
- 业务场景:客户端已登录,但没有操作资源的权限,比如 “普通用户想修改管理员账号”“员工想查看其他部门的工资”。
- 区别 401:401 是 “没登录”,403 是 “已登录但没权限”—— 这是新手最容易搞混的两个码!
(4)404 Not Found:资源不存在
- 业务场景:两种情况:① 请求路径错(比如
/api/order
写成/api/orders
);② 资源真的不存在(比如查 ID=999 的订单,实际没有)。 - 避坑:不要用 404 返回 “业务不存在”(比如 “用户名不存在”),应该用 400 或自定义业务码 ——404 更适合 “路径错” 或 “物理资源不存在”。
(5)429 Too Many Requests:请求太频繁(接口防刷)
- 业务场景:客户端短时间内请求次数太多,触发接口防刷,比如 “1 分钟内发 10 次短信验证码”“每秒刷 100 次下单接口”。
- 关键:返回 429 时,加
Retry-After
头告诉客户端 “多久后能再请求”(比如Retry-After: 60
表示 1 分钟后)。
Spring Boot 代码示例(4xx 全局异常处理)
用@ControllerAdvice
统一处理客户端错误,避免每个接口都写重复的响应码逻辑:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// 全局异常处理器:统一返回4xx响应码
@ControllerAdvice
public class GlobalExceptionHandler {
// 1. 400 Bad Request:参数校验失败(@Valid注解触发)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Map<String, String>> handleParamError(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
// 收集所有参数错误(比如“productId:商品ID不能为空”)
bindingResult.getFieldErrors().forEach(fieldError ->
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage())
);
return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}
// 2. 401 Unauthorized:未登录异常(自定义异常)
@ExceptionHandler(NotLoginException.class)
public void handleNotLogin(NotLoginException e, HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
// 加WWW-Authenticate头,告诉客户端需要传Token
response.setHeader("WWW-Authenticate", "Bearer realm="请登录获取Token"");
response.getWriter().write("{"code":401,"msg":"" + e.getMessage() + ""}");
}
// 3. 403 Forbidden:权限不足异常(自定义异常)
@ExceptionHandler(NoPermissionException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<Map<String, String>> handleNoPermission(NoPermissionException e) {
Map<String, String> result = new HashMap<>();
result.put("code", "403");
result.put("msg", e.getMessage());
return new ResponseEntity<>(result, HttpStatus.FORBIDDEN);
}
// 4. 429 Too Many Requests:请求太频繁(自定义异常)
@ExceptionHandler(TooManyRequestsException.class)
public ResponseEntity<Map<String, String>> handleTooManyRequests(TooManyRequestsException e) {
Map<String, String> result = new HashMap<>();
result.put("code", "429");
result.put("msg", e.getMessage());
// 加Retry-After头:1分钟后可重试
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", "60")
.body(result);
}
}
// 自定义异常(实际项目放单独的exception包)
class NotLoginException extends RuntimeException {
public NotLoginException(String message) {
super(message);
}
}
class NoPermissionException extends RuntimeException {
public NoPermissionException(String message) {
super(message);
}
}
class TooManyRequestsException extends RuntimeException {
public TooManyRequestsException(String message) {
super(message);
}
}
4. 5xx 服务器错误:“你请求没问题,但我这边出故障了”
核心:客户端请求是对的,但服务器内部出错(代码 bug、服务挂了、依赖超时) ,这时候要 “藏好错误细节,给用户友好提示”。高频用 3 个:500、502、503。
(1)500 Internal Server Error:通用服务器错误(最常见)
- 业务场景:服务器代码抛异常(比如空指针、数据库报错),比如 “下单接口查询库存时数据库连接超时”“代码里写了
1/0
”。 - 避坑:绝对不能把堆栈信息返回给客户端!要统一捕获异常,返回 “服务器内部错误,请稍后重试”,同时把堆栈记到日志里(方便排查)。
(2)502 Bad Gateway:网关错误
- 业务场景:微服务架构中,客户端请求经过网关(比如 Nginx、Spring Cloud Gateway),但网关转发请求时,后端服务没响应或返回错误,比如 “网关调用订单服务时,订单服务宕机了”。
- 责任方:问题在 “网关和后端服务之间”,客户端和网关的通信是好的。
(3)503 Service Unavailable:服务不可用(临时)
- 业务场景:服务器暂时没法处理请求(不是永久挂了),比如 “服务在重启”“服务触发降级(比如秒杀时流量太大,暂时关闭非核心接口)”“服务器 CPU / 内存满了”。
- 关键:返回 503 时,加
Retry-After
头告诉客户端 “多久后重试”,比如服务重启需要 10 秒,就加Retry-After: 10
。
Spring Boot 代码示例(500 统一处理)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class ServerExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(ServerExceptionHandler.class);
// 500 Internal Server Error:捕获所有未处理的异常
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleServerError(Exception e) {
// 1. 把错误堆栈记到日志(排查问题的关键!)
log.error("服务器内部错误", e);
// 2. 返回友好提示,不暴露堆栈
Map<String, String> result = new HashMap<>();
result.put("code", "500");
result.put("msg", "服务器内部错误,请稍后重试");
return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
}
// 503 Service Unavailable:服务降级时返回
@ExceptionHandler(ServiceDegradeException.class)
public ResponseEntity<Map<String, String>> handleServiceDegrade(ServiceDegradeException e) {
Map<String, String> result = new HashMap<>();
result.put("code", "503");
result.put("msg", e.getMessage());
// 告诉客户端10秒后重试
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.header("Retry-After", "10")
.body(result);
}
}
// 自定义服务降级异常
class ServiceDegradeException extends RuntimeException {
public ServiceDegradeException(String message) {
super(message);
}
}
三、8 年开发的避坑指南:这些响应码别乱用!
- 坑 1:用 200 返回所有结果,包括错误
比如登录接口 “用户名不存在” 返回 200+“错误信息”—— 前端得解析响应体才知道错了,不符合 HTTP 规范。正确做法:返回 400(参数错误)或自定义业务码(200 里带success: false
)。 - 坑 2:401 和 403 搞混
未登录返回 403,已登录没权限返回 401—— 前端会误以为 “没权限是没登录”,引导用户重复登录。记住:401 是 “没身份”,403 是 “有身份但没权限” 。 - 坑 3:500 直接抛堆栈给客户端
曾经有个项目把空指针堆栈返回给用户,用户截图发朋友圈吐槽 “这系统太烂了”。正确做法:日志记堆栈,给用户返回 “服务器内部错误,请稍后重试”。 - 坑 4:用 404 返回 “业务不存在”
比如 “查询订单 ID=999 不存在” 返回 404——404 更适合 “路径错了”(比如/api/order
写成/api/orders
)。业务不存在建议返回 200+data: null
或 400+“订单不存在”。 - 坑 5:忽略 304 的缓存作用
静态资源(CSS/JS/ 图片)每次都返回 200,浪费带宽。正确做法:用 304+Last-Modified
/ETag
,让客户端复用缓存。
四、总结:响应码的 “黄金法则”
8 年开发下来,我总结出 3 条响应码使用的黄金法则,帮你少踩 90% 的坑:
-
“对号入座” :按 “请求处理阶段” 选类别(2xx 成功、4xx 客户端错、5xx 服务器错),再选具体码;
-
“前后端共识” :和前端约定清楚每个码的含义(比如 401 代表 “Token 过期,需要重新登录”),避免歧义;
-
“隐藏细节,暴露必要信息” :客户端错(4xx)要告诉 “错在哪”(比如 “手机号格式不对”),服务器错(5xx)要隐藏细节(比如不抛堆栈),重定向(3xx)要告诉 “去哪”。
其实 HTTP 响应码不难,难的是 “养成规范使用的习惯”。下次写接口时,别再随手填 200 或 500,多想想 “这个场景该用哪个码,前端拿到后会怎么处理”—— 这才是专业开发的体现。
如果你的项目里还有 “响应码乱用” 的问题,赶紧用文中的全局异常处理器优化下吧~ 有问题欢迎评论区交流!