PHP 使用 fetch 发送 POST 后 `$_POST` 为空,最常见原因是请求体格式与 `Content-Type` 不属于 PHP 自动解析的表单类型。`$_POST` 只会在 POST 请求使用 `application/x-www-form-urlencoded` 或 `multipart/form-data` 时自动填充。若前端发送 `application/json`,数据仍在 HTTP 请求体中,但 PHP 不会把它转换到 `$_POST`;后端应读取 `php://input`,再使用 `json_decode(..., true, flags: JSON_THROW_ON_ERROR)` 解码。
修复时先决定接口契约,不要同时猜三种格式:纯 JSON API 使用 JSON 和 `php://input`;普通键值表单使用 `URLSearchParams`,PHP 从 `$_POST` 读取;包含文件时使用 `FormData`,文本字段进入 `$_POST`、文件进入 `$_FILES`,并且前端不能手工设置 multipart 的 Content-Type,否则浏览器无法添加 boundary。请求“已经发出”与“PHP 已按期望格式解析”是两个不同事实。
打开开发者工具 Network,选择请求,检查 Request Method、Request Headers 和 Payload。
重点确认 Content-Type 的完整值、请求体是否存在、键名是否正确,以及请求是否被重定向。
不要只看 fetch 代码。Service Worker、代理、框架拦截器和重定向都可能改变最终请求。
PHP 官方手册把 `$_POST` 定义为表单 POST 数据,自动支持 urlencoded 和 multipart 两类 Content-Type。
它不是“所有 POST 请求体”的通用容器。JSON、XML、纯文本和自定义二进制不会自动进入。
因此空数组并不证明网络没有数据,也不证明 fetch 失败。
前端把普通对象通过 JSON.stringify 转为字符串,设置 `Content-Type: application/json`,并使用 POST。
PHP 读取 `file_get_contents('php://input')`,解码为关联数组,然后按业务规则验证。
后端不要再从 `$_POST['name']` 取值,应从解码后的 `$data['name']` 读取。
先限制 Content-Length 和服务器请求体大小,避免无限读取。空 Body 与合法 JSON null 要区分。
使用 `JSON_THROW_ON_ERROR` 让语法错误进入异常处理,返回 400 或 422,而不是得到 null 后继续运行。
解码后确认顶层是数组或对象结构,不接受数字、字符串等与接口契约不符的 JSON 根值。
前端发送 `{name: 'Ada', active: true}` 的 JSON 字符串。浏览器不会自动序列化普通对象。
后端检查请求方法与媒体类型,读取原始流一次,解码并验证 name 是非空字符串、active 是布尔值。
响应统一设置 JSON Content-Type,并返回 JSON 对象,前端先检查 `response.ok` 再解析。
fetch 的 body 接受字符串、FormData、URLSearchParams、Blob、流等类型。普通对象可能被转换成无意义字符串。
`body: {name: 'Ada'}` 并不会自动生成 JSON,也不会生成表单编码。
必须显式选择 `JSON.stringify`、`new URLSearchParams` 或 `new FormData`。
只提交简单文本键值且希望继续使用 `$_POST` 时,可以把 body 设置为 URLSearchParams。
配合 `application/x-www-form-urlencoded;charset=UTF-8`,PHP 会按表单规则填充 `$_POST`。
数组和嵌套对象需要采用 PHP 支持的方括号字段命名,或在前端明确编码,不要依赖对象自动展开。
传统 jQuery 方法默认把对象编码为 urlencoded 表单,所以 PHP 自动填充 `$_POST`。
fetch 更底层,不会替开发者决定对象编码。迁移时若只替换函数名,Body 格式就会变化。
正确比较要看最终请求的 Content-Type 和字节内容,而不是库名称。
需要上传文件时创建 FormData,通过 append 添加文本和 File,再直接作为 fetch body。
浏览器生成 multipart/form-data 和随机 boundary。PHP 把普通字段放入 `$_POST`,文件元数据放入 `$_FILES`。
字段名必须存在且与后端一致,HTML input 的 id 不能代替 name。
MDN 明确警告,使用 FormData 时不要自行设置 Content-Type。浏览器需要在头部加入与 Body 相同的 boundary。
手工只写 `multipart/form-data` 会缺 boundary,PHP 无法切分字段,常表现为 `$_POST` 与 `$_FILES` 都为空。
删除该头,让浏览器自动生成;其他业务头仍可保留。
Header 声称 urlencoded,Body 却是 JSON 字符串时,PHP 会按错误语法解析,结果可能是异常键或空数据。
Content-Type 是对 Body 编码的声明,二者必须一致。
修复不能只改 Header;同时改序列化方式和后端解析器。
FormData 或 URLSearchParams 若被标为 JSON,后端 json_decode 会失败。
Network 面板中看起来有键值,不代表 JSON 语法成立。
契约测试应断言媒体类型和请求体,而不仅断言 HTTP 方法。
`php://input` 是只读流,提供原始请求体。它适合 JSON、XML和其他没有自动填充超全局变量的类型。
读取后得到字符串,需要使用与 Content-Type 匹配的解析器。不要把它直接拼入 SQL 或输出页面。
框架可能已经读取并解析该流,使用框架 Request API 时优先遵循框架生命周期。
中间件、日志组件和控制器如果各自读取或修改请求体,可能造成后续看到空内容或不同数据。
在入口层解析一次,生成类型化请求对象,业务层不再碰原始流。
若需要审计,只记录大小、哈希和脱敏字段,不重复消费 Body。
`$_POST` 的自动填充与 POST 方法及表单媒体类型相关。GET 数据在查询字符串中,由 `$_GET` 读取。
PUT、PATCH、DELETE 即使发送表单格式,也不会按所有 PHP 版本的传统规则自动进入 `$_POST`。
REST 接口应通过统一 Request 解析层处理,不要把 `$_POST` 当成所有方法的容器。
PHP 8.4 提供 `request_parse_body()`,主要用于解析非 POST 的 urlencoded 或 multipart Body。
它返回表单与文件数组,并受请求体解析限制影响。它不是 JSON 解析器。
请求体只能消费一次;已经读取 php://input 后再调用可能得到空数据。
真实 Header 常为 `application/json; charset=utf-8`,不能只用字符串完全相等判断。
解析分号前的媒体类型并大小写归一,参数单独处理。multipart boundary 必须保留给解析器。
不支持的媒体类型返回 415,比静默给出空数据更容易排查。
HTTP 到 HTTPS、缺斜杠或登录跳转可能改变请求方法和 Body,最终 PHP 脚本收到的是另一个请求。
Network 面板查看完整跳转链与最终 URL。后端访问日志记录方法、路径和状态码。
API 地址使用规范 URL,认证失败返回 JSON 401,不跳到 HTML 登录页。
fetch 只在网络层失败时拒绝 Promise,HTTP 400、422 或 500 仍会得到 Response。
前端必须检查 `response.ok` 或 status,再按响应 Content-Type 解析错误。
否则后端已经报告 JSON 格式错误,界面却可能把它当成功或在解析 HTML 时抛第二个错误。
设置 `response.json()` 只决定如何读取服务器响应,不会改变发送给 PHP 的请求体。
请求用 headers 和 body 定义;响应解析在 fetch Promise 完成后进行。
排查时分别记录 Request Payload 与 Response Preview,不要混为一谈。
同源 fetch 默认带同源凭据,但跨源 Cookie 需要合适 credentials、CORS 和 Cookie SameSite 配置。
会话 Cookie 缺失通常导致认证失败或新 Session,不直接决定 `$_POST` 是否解析。
后端若登录失败重定向,最终表现可能像数据消失,因此仍需检查状态与跳转。
跨域 JSON 请求通常触发 OPTIONS 预检,因为 application/json 不是简单请求媒体类型。
若服务器只处理 POST,预检失败时真正请求不会发送。此时 Network 中会看到 OPTIONS 错误。
配置明确允许 Origin、Method 和 Headers,带凭据时不能使用通配 Origin。
基于 Cookie Session 的接口仍需 CSRF Token。JSON Body 不会自动绕过 CSRF 风险。
Token 可放在受支持 Header,服务端中间件验证。不要为了让 fetch 工作就全局关闭 CSRF。
纯 Token API 使用对应认证方案,并限制 CORS 和令牌存储。
`post_max_size` 超限时,PHP 可能让 `$_POST` 和 `$_FILES` 为空。上传场景还受 upload_max_filesize 影响。
比较 Content-Length 与服务器限制,在入口返回 413,而不是继续报告“字段缺失”。
代理、Web Server 和 PHP 各自可能有大小限制,配置需要一致。
大型表单字段过多时,PHP 可能因 max_input_vars 截断后续变量,不一定整个数组为空。
嵌套动态表单应控制字段数量,API 大对象更适合 JSON 和明确 Schema。
不要只提高上限;先确认是否把可压缩数据错误拆成数万个表单字段。
FormData 从表单构建时,只包含成功控件。没有 name、被禁用或未选中的控件可能不提交。
`id="email"` 只服务 DOM 与标签关联,PHP 使用的是 name 值。
直接打印 FormData 对象不一定显示内容,可遍历 entries 或查看 Network Payload。
PHP 表单语法使用 `items[]` 或 `user[name]` 形成数组。重复普通键的行为可能不符合预期。
JSON 天然支持数组与对象,复杂结构优先使用 JSON。
后端无论哪种格式都应验证最大深度、元素数量和每项类型。
表单值通常进入 PHP 为字符串,`false` 字符串并不是布尔 false。JSON 可以保留布尔和数字类型。
不能因类型差异在业务层使用宽松比较。使用 filter、Validator 或显式转换。
接口文档说明编码与字段类型,让前后端对空字符串、null 和缺失有一致理解。
开发环境可临时输出 method、Content-Type、Content-Length、`$_POST` 键名和原始 Body 长度。
不要在生产返回完整请求体或 Header,其中可能含密码、Cookie 和 Token。
调试完成后删除端点,避免它成为数据泄露接口。
`$_REQUEST` 混合 GET、POST 和 COOKIE,来源与优先级受配置影响,仍不会神奇解析 JSON。
使用它会让查询参数或 Cookie 覆盖预期 Body 字段,增加安全与排查难度。
每个接口明确从一种输入来源读取。
multipart 包含 boundary、编码、文件头和临时文件处理,手工按字符串切分容易出错与产生安全漏洞。
标准 POST 让 PHP 自动填充;PHP 8.4 非 POST 可使用 request_parse_body。
JSON 不包含文件二进制时,使用标准 JSON 解析器。
Laravel、Symfony 等框架通常提供 `$request->json()`、input 或 DTO 映射,封装媒体类型解析。
不要同时读取框架 Request 与原始流后期望两份独立 Body。遵循框架文档和中间件顺序。
原生 PHP 教程中的 `$_POST` 规则仍解释底层现象,但业务代码应使用框架抽象。
缺 Content-Type 返回 415 或明确错误;无效 JSON 返回 400;字段验证失败返回 422。
响应包含稳定 error code 和字段错误,不回显完整敏感输入。
前端根据状态显示问题,不能把所有异常归类为网络断开。
记录 request_id、method、path、media_type、body_bytes、解析结果类型和错误代码。
不要默认记录原始 JSON、multipart 文件、Cookie 或 Authorization。
用 request_id 关联浏览器报错、代理访问日志和 PHP 应用日志。
为 JSON 解析器测试合法对象、空 Body、无效 JSON、错误根类型、超深结构与大整数。
为业务验证测试缺字段、错误类型和边界长度。解析与业务规则分开测试。
不要通过直接给 `$_POST` 赋值来代表 JSON 请求,那会绕过真正问题。
真实发送三类请求:JSON、urlencoded 和 multipart,断言后端分别从正确入口获得数据。
测试 multipart 时让 HTTP 客户端生成 boundary,不手工伪造不完整 Header。
再测试错误 Content-Type 与 Body 组合,确认服务端返回 415 或 400。
从实际页面提交 fetch,检查 DevTools 中 Header、Payload、Cookie、预检与响应。
覆盖同源和部署后的真实跨域拓扑。开发代理可能掩盖生产 CORS 问题。
测试快速双击、网络重试和页面跳转,确保请求没有被意外取消或重复提交。
组合一:JSON.stringify 加 application/json,PHP 读取 php://input 并 json_decode。
组合二:URLSearchParams 加 urlencoded,PHP 读取 `$_POST`。
组合三:FormData 且不手设 Content-Type,PHP 读取 `$_POST` 与 `$_FILES`。
先确认 Network 中真正发出了 POST 和 Body,再确认 Content-Type 与 Body 编码一致。
然后检查后端选择了对应解析入口、请求未被重定向、没有被中间件提前消费。
最后检查 CORS、CSRF、会话、Body 大小和字段名,不要一开始随意修改 PHP 配置。
确认接口只接受文档声明的媒体类型,不支持类型返回 415。
确认 JSON 使用 php://input 与异常解码,表单编码才使用 `$_POST`。
确认 FormData 未手工设置 multipart Header,文件从 `$_FILES` 读取。
确认请求体限制、CORS、CSRF、认证、错误状态和日志脱敏均已测试。
确认前端检查 response.ok,并能显示后端返回的结构化错误。
fetch POST 后 `$_POST` 为空,通常不是 fetch 没发送,而是 PHP 按 Content-Type 选择了不同解析路径。
发送 JSON 就读取 php://input;需要 `$_POST` 就发送 URLSearchParams 或 FormData。FormData 的 multipart boundary 必须交给浏览器生成。
把方法、媒体类型、Body 编码和后端解析器作为一个完整契约,再按 Network、重定向、CORS、CSRF 与大小限制依次排查,问题就不会停留在“为什么数组是空的”这个表象。
PHPUnit 如何为 preg_match 的失败分支实现 100% 代码覆盖率?
花云多个节点访问返回 403,是节点 IP 被限制了吗?
Claude Opus 4、GPT-5 与其他模型在端到端应用开发基准中表现如何?
SQL MCP Server 如何让每次数据库写操作都经过人工批准?
ubuntu17.10桌面回收站怎么删除?
Qwen Code 添加 Qwen3.7-Max 后为什么模型测试通过但连接总是超时?