返回博客

ThinkPHP success() 被 catch 吃掉:HttpResponseException 的反直觉陷阱

某次排查某后台管理系统的 API 时,遇到一个让人抓狂的现象:数据库查询正常、业务逻辑无误、日志也写进去了,但接口就是稳定返回 {"code":400,"msg":"操作失败"}。换一个 controller 全好,但这个 controller 里的所有方法都是 400——不管传什么参数。

现象

  • 接口始终返回错误响应,HTTP 200 但业务码 400
  • 日志里 $e->getMessage() 打印出来的内容竟然是 "ok"(成功提示语)
  • 同一 controller 下所有方法无一例外,换 controller 立刻正常
  • 业务逻辑单独跑(如在命令行查数据库)完全没问题

根因

ThinkPHP(5.x / 6.x / 8.x 均适用)的 $this->success()$this->error() 不是普通的 return——它们内部通过抛出 HttpResponseException 来终止控制器执行,框架在更上层捕获这个异常并输出响应。

继承链是:

HttpResponseException
  → RuntimeException
    → Exception
      → Throwable

因此,只要 $this->success()try 块内,后面跟着 catch (\Throwable $e),成功响应就会被截胡:

public function getList(): void
{
    try {
        $data = $this->someQuery();
        $this->success('ok', $data);   // 抛出 HttpResponseException
    } catch (\Throwable $e) {          // 把上面那个异常接住了!
        Log::error('失败: ' . $e->getMessage());  // 打印出 "ok"
        $this->error('操作失败');      // 覆盖掉成功响应
    }
}
坑:success() 在 try 块里会被 catch(\\Throwable) 截胡

$this->success() 本质是 throw new HttpResponseException(...),它不是 return。catch (\Throwable) 的捕获范围涵盖所有异常,包括 HttpResponseException。结果:业务成功 → 框架抛 success 异常 → catch 接住 → 打印日志(message 是"ok") → 再抛 error 异常 → 接口返回 400。

同理,$this->error() 在 try 块里也有同样问题——例如参数校验失败时调用 error(),同样会被后面的 catch 二次捕获。

正解

在每一个 catch (\Throwable) 之前,先插入一个专门捕获 HttpResponseException 的子句,让框架的响应机制正常透传:

<?php
use think\exception\HttpResponseException; // 1. 必须 import
 
public function getList(): void
{
    try {
        $data = $this->someQuery();
        $this->success('ok', $data);
    } catch (HttpResponseException $e) {   // 2. 先于 \Throwable 捕获
        throw $e;                          // 3. 原样抛出,让框架处理
    } catch (\Throwable $e) {
        Log::error('操作失败: ' . $e->getMessage());
        $this->error('操作失败');
    }
}

PHP 的异常捕获按顺序匹配,第一个匹配的 catch 获胜。将 HttpResponseException 放在 \Throwable 前面,框架的 success/error 响应就能正常穿透,真正的运行时异常仍然被 \Throwable 兜住。

正解:HttpResponseException 必须在 \\Throwable 之前重新抛出

标准模板:

use think\exception\HttpResponseException;
 
try {
    // 业务逻辑
    $this->success('ok', $data);
} catch (HttpResponseException $e) {
    throw $e;  // 让框架响应机制正常工作
} catch (\Throwable $e) {
    Log::error('xxx: ' . $e->getMessage());
    $this->error('操作失败');
}

新建 controller 时把这个三段式作为默认模板,从源头杜绝。

排查

怀疑整个 controller 都被污染时,用这条命令快速定位受影响文件:

# 找出同时含有 catch(\Throwable) 且调用 $this->success 的 controller
grep -rl 'catch.*\\Throwable' app/api/controller/ | xargs grep -l '\$this->success'

一句话外卖

ThinkPHP 的 success()/error() 是用异常实现的响应机制,凡是在 try 块里调用它们,就必须在 catch (\Throwable) 前先加 catch (HttpResponseException $e) { throw $e; },否则响应会被静默截胡。