欢迎访问晨星博客!
接口错误处理最容易失控的地方,往往不是错误本身,而是同一类错误在不同接口里返回不同字段、不同状态码,甚至把异常信息直接交给客户端。维护时可以把边界划清:业务代码只负责识别问题并抛出明确异常,接口出口统一决定 HTTP 状态码、响应结构和对外消息。

建议把接口中的错误分成三类。参数校验失败表示请求内容不符合接口要求,例如缺少必填字段;业务异常表示请求格式正确,但当前业务条件不允许执行;未预期错误则是程序无法按预期完成处理的问题,通常不应向客户端暴露内部细节。
这三类情况需要不同的对外消息,却不应各自拼装响应。可以让前两类使用明确的应用异常表达,并由统一出口转换;其余未处理异常则统一落入兜底分支。这样业务逻辑不必在多个位置重复判断 HTTP 状态码和 JSON 字段。
响应结构可以保持简单且稳定:
{
"code": "INVALID_ARGUMENT",
"message": "请求参数不合法",
"data": {}
}
code 用于客户端识别错误类型,message 提供适合展示的说明,data 在错误时仍保留为空对象,减少客户端对字段是否存在的分支判断。HTTP 状态码则由响应层单独设置,不要把它和业务错误码混为一谈。
HTTP 状态码描述这次请求在协议层面的处理结果;响应体中的 code 描述应用层遇到的具体问题。比如,参数不符合接口要求时可采用 400,未预期错误使用 500。项目应先约定映射规则,再由统一处理器执行,而不是让每个接口临时决定。
业务异常的状态码应结合实际语义选择。若错误表示请求本身无法被接受,可以沿用项目约定的客户端错误状态;若是资源不存在等不同情形,也应按对应语义处理。不要为了让客户端总是读取响应体中的 code,就把所有失败都伪装成 HTTP 200:这会让依赖 HTTP 状态判断的客户端、代理或监控逻辑难以区分失败。
一旦项目已有稳定约定,应优先保持兼容。调整状态码或响应字段时,需把它视作接口变更,而不是单纯的内部重构。
下面的例子使用原生 PHP 演示最小组织方式,不依赖具体框架。可以先将代码放在单个 PHP 文件中运行;项目变大后,再按异常类、响应函数和接口入口拆分文件。
<?php
class ApiException extends RuntimeException
{
private $httpStatus;
private $errorCode;
public function __construct(
int $httpStatus,
string $errorCode,
string $message
) {
parent::__construct($message);
$this->httpStatus = $httpStatus;
$this->errorCode = $errorCode;
}
public function getHttpStatus(): int
{
return $this->httpStatus;
}
public function getErrorCode(): string
{
return $this->errorCode;
}
}
class ValidationException extends ApiException
{
public function __construct(string $message = '请求参数不合法')
{
parent::__construct(400, 'INVALID_ARGUMENT', $message);
}
}
class BusinessException extends ApiException
{
public function __construct(string $message)
{
parent::__construct(400, 'BUSINESS_ERROR', $message);
}
}
function sendJson(int $status, array $body): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(
$body,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
}
function sendError(Throwable $exception): void
{
if ($exception instanceof ApiException) {
sendJson($exception->getHttpStatus(), [
'code' => $exception->getErrorCode(),
'message' => $exception->getMessage(),
'data' => (object) [],
]);
return;
}
// 未预期异常的详细信息不返回给客户端。
sendJson(500, [
'code' => 'INTERNAL_ERROR',
'message' => '服务器暂时无法处理请求',
'data' => (object) [],
]);
}
try {
$name = $_POST['name'] ?? null;
if (!is_string($name) || trim($name) === '') {
throw new ValidationException('请填写名称');
}
if ($name === 'restricted') {
throw new BusinessException('当前操作不符合业务规则');
}
sendJson(200, [
'code' => 'OK',
'message' => '成功',
'data' => ['name' => trim($name)],
]);
} catch (Throwable $exception) {
sendError($exception);
}
参数检查和业务判断放在接口处理逻辑中,分别抛出对应异常;sendError() 集中处理异常到 HTTP 响应的转换。真正的项目可以把 ApiException、具体异常类、响应封装和接口入口拆开,但转换规则仍只维护一份。
示例中的业务异常也使用 400,是为了展示最小映射,并不意味着所有业务问题都必须使用同一状态码。实际项目应根据错误含义制定一致的规则;若某些业务情形需要不同状态码,可在业务异常中携带经过约定的状态码和错误标识,而不是在控制器里临时拼响应。
参数错误和业务异常的消息可以返回给客户端,但应由应用主动构造。不要把 SQL 错误、文件路径、堆栈信息或原始异常消息直接塞进响应;未预期异常统一返回不包含内部细节的提示。
隐藏细节不等于丢弃诊断信息。生产环境应在服务端通过项目已有的异常记录方式保存必要信息,供维护者排查,并避免把这些内容混入公开响应。对外响应负责稳定和安全,内部记录负责定位问题,两者应分开处理。
还要注意,catch (Throwable) 能处理抛出的异常与错误对象,但不能简单理解为所有 PHP 警告都会自动进入这个分支。若项目需要统一处理警告,应另行明确错误转换策略,避免误以为兜底捕获已经覆盖所有运行时问题。
改造已有接口时,可以先盘点现有错误响应,确定字段、错误码和状态码约定,再逐步把重复的 try-catch 与散落的 JSON 拼装迁移到统一出口。控制器保留必要的业务判断,响应格式和敏感信息处理则集中维护。
这套边界能否长期有效,取决于团队是否坚持同一约定:已知错误明确分类,未预期错误安全兜底,所有接口从同一个出口生成错误响应。
声明:原创文章请勿转载,如需转载请注明出处!