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-Type 为 application/x-www-form-urlencoded 或 multipart/form-data 时才被填充。 JSON body 停留在原始输入流 php://input 里,$_POST 永远是空数组。
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 而非精确匹配,因为实际请求头经常带 ; 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 测试却全绿。