ChatGPT 是只不错的 “小黄鸭”

2023-09-06

今天遇到一个问题,在使用 Symfony 6.x 框架的项目里,我没有使用框架自带的 Security 组件,而是自己实现了一个用户认证的土方法。

在我的方法中,当用户请求需要登录的接口时,会抛出一个自定义的异常 ApiNotAuthException,然后我在 ExceptionListener 监听器服务中捕获这个异常并封装我自己的返回值结构,最后将状态码强制改为 200(因为我不喜欢使用 HTTP 状态码来替代业务状态码)。

PS: 关于是否应该忽略 HTTP 状态码在前端的处理以及是否应该将业务状态码封装到 HTTP 状态码里,以后有机会我再写一篇博客讨论一下,今天只解决问题。

我的异常监听类是这样的:

final class ExceptionListener
{
     public function onKernelException(ExceptionEvent $event)
    {
        $e = $event->getThrowable();

        if ($e instanceof ApiNotAuthException) {

            $response = new JsonResponse([
                'success' => false,
                'code'    => $e->getCode(), // 我的业务状态码
                'msg'     => $e->getMessage(),
                'data'    => null,
            ], Response::HTTP_OK); // 初始化时设置状态码为 200

            $response->setStatusCode(Response::HTTP_OK); // 初始化之后再次设置状态码为 200
            $response->headers->set('X-Status-Code', Response::HTTP_OK);
            $event->setResponse($response);
        }
    }
}

这样做的目的是为了让前端可以统一处理业务状态码,而不用关心 HTTP 状态码。 此外因为我觉得 HTTP 状态码应该是用来表示请求的状态,而不是业务状态,而登录与否应该属于业务层面。

总之就是状态码的改变没效果,HTTP Headers 中的 X-Status-Code 有效果,说明代码确实执行了。

我猜测就是某个默认配置的问题,导致我的这个设置被覆盖了,但官方文档看了一遍,网上搜了一圈,也没找到解决方案。

最后没办法了,我就想试试 ChatGPT。

我们之间的第一次对话是这样的:

文字版本的在 这里

中间 ChatGPT 提出了服务优先级的问题,这个与我之前的猜测是一致的,但尝试了没有效果,无论是设置为最高还是最低。

最终 ChatGPT 给了我一个不太优雅的解决方案,虽然我对结果不太满意,但对话过程中的回答还是蛮让我惊艳的,因为它似乎已经完全看懂了 Symfony 官方文档 以及 StackOverflow 上的一些问答。

我回想了一下,觉得是不是因为我给它设置的人设( “高级软件工程师”、“熟悉 Symfony 框架” )不够强大,所以没给出一个让我满意的解决方案?

于是我和 ChatGPT 开始了第二次对话:

第二次对话的文字版本的在 这里

这次对话我将 ChatGPT 的人设提升到了 “软件技术专家”,感觉确实感觉不一样,可以明显看到代码变少了,文字变多了,也许这就是 “高级软件工程师” 和 “软件技术专家” 的区别?工程师更多的给代码,专家更多的给思路?那 Linus 的 "Talk is cheap, show me the code" 怎么解释?哈哈哈。

根据 ChatGPT 给我的各种可能性提示,我逐一排除,最终推测可能是框架内核里有什么特殊逻辑导致的。

直接看 Symfony\Component\HttpKernel\HttpKernel 类的 handle 方法,这个方法是框架内核的核心方法,所有的请求最终都是在这里处理的,它会调用 handleThrowable 来处理异常。此时,我发现了端倪。

handleThrowable 方法中,有这样一段代码:

$response = $event->getResponse();

// the developer asked for a specific status code
if (!$event->isAllowingCustomResponseCode() && !$response->isClientError() && !$response->isServerError() && !$response->isRedirect()) {
    // ensure that we actually have an error response
    if ($e instanceof HttpExceptionInterface) {
        // keep the HTTP status code and headers
        $response->setStatusCode($e->getStatusCode());
        $response->headers->add($e->getHeaders());
    } else {
        $response->setStatusCode(500);
    }
}

这里注释说明了当开发者需要一个特殊的状态码时的处理逻辑。这个逻辑很简单,就是判断异常类是否实现了 HttpExceptionInterface 接口,如果没有,一律 500;有的话,从异常里获取 HTTP 状态码和头部信息赋值给 Response 对象。

看来框架是想将异常错误码也就是我认为的业务状态码当做 HTTP 状态码来处理(我不喜欢也不需要),如果不符合 HTTP 规范则一律判定为 500,太残暴了。

所以解决问题的方案就是让自定义异常类实现这个接口就行了。之前修改不生效的原因就在这里,当然也可以认为是我的代码在这段代码之前执行了,也就是优先级太高,问题是高低我都试过了,都失败了。 既然如此,那就不纠结执行的优先级的问题了,直接实现接口就行了。

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class ApiNotAuthException extends \RuntimeException implements HttpExceptionInterface
{
    public function __construct($message = "未登录", $code = 1, \Throwable $previous = null)
    {
        parent::__construct($message, $code, $previous);
    }

    public function getStatusCode(): int
    {
        return Response::HTTP_OK; // 直接返回 200
    }

    public function getHeaders(): array
    {
        return [];
    }
}

再次测试,问题解决。

总结一下,虽然 ChatGPT 并没有直接给出解决方案(因为没有用 GPT4 尝试,所以不知道 GPT 最强实力),但是它的提示已经很专业很厉害了。

以前我调试的时候喜欢找个 “小黄鸭” 来边说边操作,因为在说的过程中,由于对方不了解细节,你需要详细描述你是怎么做的,以及为什么这么做,会慢慢的把问题说清楚,往往自己说着说着,就发现问题所在了,所以从我的经验来说 “小黄鸭调试法” 确实很有效果。

在电脑旁“帮忙”代码审查的小黄鸭

但如果只有一个人的时候,就只能对着图中的小黄鸭说话,此时总是感觉会有点奇怪。可能是因为面对的并非真人,说话的时候就不会那么严谨,期间也没有互动,对方也不会提问,导致效果不佳。

而 ChatGPT 不仅解决了说话对象的问题,甚至因为它什么都“懂”,还能给你一些很有价值的提示,甚至你还可以指定它的专业程度,简直就是一个完美的 “小黄鸭” 啊!

PS:本文的写作有几句话是由 GitHub Copilot 提示完成的,所以你可能感觉有些句子的语气并非完全是我的风格。