返回博客

axios 默认 JSON Body,PHP $_POST 却是空的——前后端参数透传的静默陷阱

前端选了三个筛选项,DevTools Network 里 Request Payload 一清二楚:store_code: "A,B,C"。刷新页面——返回的还是全量数据。换 curl 用 -d 'store_code=A,B,C' 打后端,过滤生效了。再在浏览器试,还是不对。

这不是缓存问题,也不是前端没发请求。是 PHP $_POST 的一个反直觉行为,在 axios 主导的 SPA 时代几乎每个人都会踩一次。

现象

  • 用户在页面多选 N 个值,DevTools 的 Request Payload 中参数字段完整显示。
  • 后端接口返回的仍是未过滤的全量数据集,筛选形同虚设。
  • curl -d 'field=val' 直接打同一个接口,过滤正常生效——给开发者虚假的"已通过"信心。
  • 把参数从字符串改成数组后,PHP 8+ 下 FastAdmin / ThinkPHP 5 的 request->param() 直接返回 500:variable type error: array

根因

两个独立事实叠加,造成静默失败:

事实一:axios 默认 Content-Type: application/json 不需要任何显式配置,axios POST 的 body 就是 JSON。

事实二:PHP $_POST 只在 Content-Typeapplication/x-www-form-urlencodedmultipart/form-data 时才被填充。 JSON body 停留在原始输入流 php://input 里,$_POST 永远是空数组。

坑:用 $_POST 直接读 axios 发来的参数——静默丢失

ThinkPHP 的 request->param() 通常会自动解码 JSON body,所以框架层没问题。但若为了绕过 PHP 8+ 下 FastAdmin _initialize 过滤链对数组类型抛出的 500,改成直接读 $_POST['field'],就掉进了这个陷阱:curl form-urlencoded 测试全部通过,真实 axios 请求完全无效。

更隐蔽的是:php://input一次性流,多次读取第二次返回空字符串。若多个参数都需要读 JSON body,必须缓存首次解码结果。

正解

按优先级建立多来源级联读取,同时处理好流的一次性问题:

private function resolveFilterParam(string $key): array
{
    $raw = null;
 
    // 1. JSON body(axios 默认,SPA 主流)
    $contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
    if (stripos($contentType, 'application/json') !== false) {
        $body = file_get_contents('php://input');  // 只读一次,多参数场景须缓存
        if ($body !== false && $body !== '') {
            $json = json_decode($body, true);
            if (is_array($json) && array_key_exists($key, $json)) {
                $raw = $json[$key];
            }
        }
    }
 
    // 2. form-urlencoded / multipart(jQuery、原生表单)
    if ($raw === null && array_key_exists($key, $_POST)) {
        $raw = $_POST[$key];
    }
 
    // 3. GET querystring(导出链接、调试入口)
    if ($raw === null && array_key_exists($key, $_GET)) {
        $raw = $_GET[$key];
    }
 
    // 4. request->param() 兜底——仅标量,避免 PHP 8+ 数组 filter 500
    if ($raw === null) {
        try {
            $p = $this->request->param($key, '');
            if (is_scalar($p)) $raw = $p;
        } catch (\Throwable $e) {
            $raw = '';
        }
    }
 
    if ($raw === null) $raw = '';
 
    // 统一规范化为去空格的非空字符串数组
    $values = is_array($raw)
        ? array_map('strval', $raw)
        : explode(',', (string)$raw);
 
    return array_values(array_filter(array_map('trim', $values), fn($s) => $s !== ''));
}

级联顺序的逻辑:JSON 覆盖现代 SPA,$_POST 覆盖 legacy 表单,$_GET 覆盖导出/书签链接,request->param() 作为框架兜底但只信任标量。

正解:stripos 检测 Content-Type,file_get_contents 读 php://input,缓存后复用

stripos 而非精确匹配,因为实际请求头经常带 ; charset=utf-8 后缀。php://input 仅读一次,将 json_decode 结果存到控制器属性,同一请求其他参数直接复用,不重复读流。

测试矩阵——curl 必须覆盖真实 Content-Type

| 场景 | curl 写法 | 命中来源 | |------|-----------|---------| | JSON 字符串(axios 默认) | -H "Content-Type: application/json" -d '{"field":"A,B,C"}' | JSON body | | JSON 数组 | -H "Content-Type: application/json" -d '{"field":["A","B"]}' | JSON body | | form-urlencoded | -d 'field=A,B,C' | $_POST | | GET querystring | -G --data-urlencode 'field=A,B' | $_GET | | 空 body | -H "Content-Type: application/json" -d '{}' | 全部落空 → 空数组(正确) |

只测 -d 'foo=bar' 就宣告通过是反模式——它走的是 $_POST,永远不会暴露 JSON body 分支的 bug。

一句话外卖

axios 的默认值是 application/json,PHP 的 $_POST 对 JSON 视而不见——只要两边凑在一起,参数就静默蒸发,curl 测试却全绿。