REST API POST请求与功能错误处理 以用户创建接口为例
核心设计原则
所有错误场景统一通过Spring Boot全局错误处理返回标准JSON格式响应,严格对齐HTTP状态码的原始语义,让调用方仅通过状态码就能预判响应内容结构,不需要先解析响应体判断是否出错。
场景1:待创建用户已存在的处理
基础处理逻辑
必须通过全局错误处理统一返回JSON格式的功能异常,禁止返回框架默认的HTML错误页,避免调用方解析失败。
状态码选型
直接排除303状态码的适用性:不少人误以为用户已存在可以返回303引导客户端去拉取已有用户详情,本质是完全搞错了303的使用场景。303的原始语义是引导客户端使用GET方法访问指定URI获取资源,本质是重定向行为,不是错误反馈。多数HTTP客户端收到303后会静默完成跳转,不会将该结果传递给上层业务逻辑,最后可能出现服务端已经拦截了重复创建,客户端还误以为创建成功的问题,根本达不到让调用方识别“创建失败”的目的。
最终选型为409 Conflict:该状态码的语义是「当前请求与服务端现有资源状态存在冲突,无法完成操作」,完全匹配“用户已存在、无法重复创建”的场景——请求本身格式合规、规则校验全部满足,只是服务端已存在同标识的资源,属于典型的状态冲突类问题。
对应响应体结构
409属于资源状态冲突类错误,不存在字段级的校验问题,响应体不需要携带逐字段错误明细,结构统一为:
{ "errorCode": "USER_ALREADY_EXISTS", "message": "待创建的用户名已被占用", "traceId": "全链路追踪ID" }
调用方收到409状态码时,可直接判定当前为资源冲突问题,仅需根据错误码引导用户更换用户名重试即可。
场景2:触发业务规则校验错误的处理
基础处理逻辑
同样纳入全局错误处理的统一返回范围,禁止透传框架默认的校验错误页。
状态码选型
最终选型为400 Bad Request:该状态码的语义是「请求本身存在格式、规则不符合约定的问题,服务端无法处理」,完全匹配“用户名包含空格”这类校验失败的场景——请求的内容本身就不符合接口约定的业务规则,和服务端现有资源状态无关。
这里要和409做清晰的边界划分,避免调用方逻辑混乱:
- 409:请求本身合规、校验全部通过,只是和服务端现有资源状态冲突
- 400:请求本身内容不符合规则,属于无效请求
这种边界划分刚好能实现你要的效果:调用方拿到响应的第一反应是看状态码,根本不需要先拆JSON判断是不是错,就知道该按什么逻辑处理、该解析哪部分内容。
对应响应体结构
400属于参数校验类错误,必须携带逐字段的错误明细,方便调用方将错误提示直接映射到对应输入项,响应结构为:
{ "errorCode": "VALIDATION_FAILED", "message": "请求参数不符合校验规则", "traceId": "全链路追踪ID", "fieldErrors": [ { "field": "username", "message": "用户名不允许包含空格" } ] }
调用方收到400状态码时,可直接解析fieldErrors数组,将对应字段的错误提示展示给前端用户。
落地注意点
- 自定义业务异常类时,在类上通过
@ResponseStatus注解绑定对应HTTP状态码,全局异常处理器捕获后直接返回标准化JSON结构 - 禁止将所有错误统一包装为200状态码返回,否则会完全失去状态码对调用方的引导作用
内容的提问来源于stack exchange,提问作者Emilien Brigand

