Skip to content

错误处理 ​

框架的错误响应统一长这样:

json
{
  "code": 40001,
  "message": "文章不存在",
  "data": null,
  "request_id": "01a18f94-...",
  "timestamp": 1793380598
}

code 是业务码(不是 HTTP 状态码)。两套码并存,见下文。

抛异常,不要拼响应 ​

php
// ✅ 抛异常
throw new HttpException(404, ErrorCode::NotFound->value, '文章不存在');

// ❌ 手工拼响应
return Response::json(['code' => 40001, 'message' => '文章不存在'], 404);

手工拼会漏掉:

  • request_id(没法关联日志)
  • timestamp
  • debug 模式下的堆栈
  • 统一日志记录

异常体系 ​

text
Throwable
└── HttpException                    带 status + errorCode,不会被当 500
    ├── ValidationException          422 + errors 明细
    ├── MethodNotAllowedException    405 + 自动写 Allow 头
    └── 你的业务异常(推荐继承 HttpException)

关键区别:

类型处理
HttpException 子类用它的 status / code / message,不记 error 日志
其他 Throwable一律 500 + 10001,记 error 日志,生产环境隐藏原始信息

业务异常继承 HttpException 的意义:预期内的失败不该污染错误日志。

错误码分段 ​

39 条,按段位划分:

1xxxx 系统级 ​

码常量HTTP含义
10001InternalError500服务内部错误
10002ServiceUnavailable503服务不可用
10003ConfigError500配置错误
10004BootstrapFailed500启动失败
10005Maintenance503维护中

2xxxx 认证与权限 ​

码常量HTTP含义
20001Unauthenticated401未登录
20002TokenExpired401token 过期
20003Forbidden403无权限
20004AccountDisabled403账号被禁用
20005TooManyRequests429请求过于频繁
20006SignatureExpired401签名过期
20007SignatureReplayed401签名重放
20008SignatureInvalid401签名无效
20009CredentialsInvalid401账号或密码错误
20010TokenDomainMismatch401token 不属于当前域
20011SignedUrlExpired403签名链接失效

3xxxx 参数校验 ​

码常量HTTP含义
30001ValidationFailed422参数校验失败
30002InvalidJson400JSON 格式错误
30003MissingParameter400缺少必需参数
30004InvalidParameterType400参数类型错误
30005UnsupportedMediaType415不支持的媒体类型
30006NotAcceptable406无法满足 Accept
30007MissingIdempotencyKey400缺少幂等键

4xxxx 资源 ​

码常量HTTP含义
40001NotFound404资源不存在
40002RouteNotFound404路由不存在
40003MethodNotAllowed405方法不允许
40004AlreadyExists409资源已存在
40005StateConflict409状态冲突
40006ResourceGone410资源已删除
40007PayloadTooLarge413请求体过大
40008DomainNotFound404API 域不存在
40009IdempotencyConflict409幂等键冲突
40010VersionConflict409版本冲突

6xxxx 依赖服务 ​

码常量HTTP含义
60001DatabaseError500数据库错误
60002UploadFailed500上传失败
60003CacheError500缓存错误
60004StorageError500存储错误
60005UpstreamError502上游服务错误

有些码是预留的,框架不会自己抛出,由业务按需使用:

码场景
20009 CredentialsInvalid登录失败(也可以用 422,看你想让前端「跳登录页」还是「标红输入框」)
30003 MissingParameter手写参数校验时缺字段
30004 InvalidParameterType手写参数校验时类型不对

用 FormRequest 时这三类都会被 ValidationFailed(30001 / 422)统一处理, 所以它们只在「手工校验」或「特殊业务语义」时用。


为什么没有 5xxxx

留给业务自定义。多域模式下每个域还会有自己的段位(error_segment), 比如 admin 域配了 51,它的业务错误就是 51001、51002……

定义业务异常 ​

php
<?php

declare(strict_types=1);

namespace App\Domain\Order\Exception;

use SunsetReverie\Exception\ErrorCode;
use SunsetReverie\Exception\HttpException;

final class OrderException extends HttpException
{
    public function __construct(
        string $message,
        int $status = 422,
        ErrorCode $code = ErrorCode::ValidationFailed
    ) {
        /*
         * 404 必须配 NotFound 码,否则会出现「HTTP 404 + 业务码 30001」
         * 这种矛盾:前端按状态码走是「跳详情页」,按业务码走是「标红表单字段」。
         *
         * 按状态兜底,避免每个调用点都要记得传对码。
         */
        parent::__construct(
            $status,
            $status === 404 ? ErrorCode::NotFound->value : $code->value,
            $message
        );
    }
}

用:

php
// 查询单条资源不存在 → 404
throw new OrderException(sprintf('订单 %s 不存在', $orderNo), 404);

// 提交的参数有问题 → 422(默认)
throw new OrderException('库存不足');

这个兜底很有必要

不写兜底的话,每个调用点都要记得传对业务码:

php
throw new OrderException('订单不存在', 404, ErrorCode::NotFound);   // 忘了第三个参数 → 404 + 30001

HTTP 状态和业务码语义矛盾,前端两套判断会打架。

同样的坑在 FileException、RbacException 都踩过——它们后来都加了这个兜底。

生产环境脱敏 ​

情况开发(APP_DEBUG=true)生产
HttpExceptionmessage 原样 + debug 字段message 原样(业务信息本就该给用户看)
其他异常message + 堆栈 + 文件行号只返回「服务内部错误」
日志不记(HTTP 异常)全部记

生产环境下非 HttpException 的响应:

json
{
  "code": 10001,
  "message": "服务内部错误",
  "data": null,
  "request_id": "01a18f94-..."
}

原始异常信息去哪了:日志里,用 request_id 对:

bash
grep "01a18f94" storage/logs/app-2026-10-31.log

405 的特殊处理 ​

MethodNotAllowedException 会自动带 Allow 头:

php
if ($e instanceof MethodNotAllowedException) {
    return Response::json($payload, 405)
        ->withHeader('Allow', implode(', ', $e->allowed));
}
text
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST

这是 HTTP 规范要求,也让调用方能自己纠正。

常见错误 ​

500 但看不到原因 ​

生产环境不返回堆栈。两个办法:

bash
# 1. 看日志(用响应里的 request_id 对)
grep "<request_id>" storage/logs/app-$(date +%Y-%m-%d).log

# 2. 临时开 debug(仅本地)
# .env 里 APP_DEBUG=true

业务异常被当成 500 ​

说明没继承 HttpException:

php
class OrderException extends \Exception        // ❌ → 500 + 10001
class OrderException extends HttpException     // ✅ → 用自己的 status

错误码和 HTTP 状态语义矛盾 ​

text
HTTP 404 + 业务码 30001(参数校验失败)

前端两套逻辑打架。检查业务异常的构造是否传对了码(或加 404 兜底)。

校验失败的 details 丢失 ​

ValidationException 的构造要传 errors:

php
throw new ValidationException([
    'title' => ['标题至少 2 个字符'],
]);

响应里才会有:

json
{
  "code": 30001,
  "message": "参数校验失败",
  "errors": { "title": ["标题至少 2 个字符"] }
}

下一步 ​

  • 控制器 —— 参数注入失败时抛什么
  • 中间件 —— 在控制器之前拦截并抛异常

以 MIT 许可发布 · v1.0 夕照