curlopt_returntransfer 必须设为 true,否则无法捕获流式响应;需配合 curlopt_writefunction 解析 text/event-stream 格式,剥离 data: 前缀后 json 解码,并关闭 php 和 nginx 的输出缓冲。

PHP 8.5 调用 OpenAI API 时 cURL 报错 CURLOPT_RETURNTRANSFER 必须设为 true
流式输出(streaming)不是靠开关打开的,而是靠逐块读取响应体实现的;但很多人一上来就设 curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: text/event-stream']),却忘了 CURLOPT_RETURNTRANSFER 若为 false,curl_exec() 就直接输出到 stdout,根本拿不到数据流——更别说解析了。
常见错误现象:Warning: curl_exec(): Cannot use output buffering in output buffering display handlers,或压根没返回、页面空白。
-
CURLOPT_RETURNTRANSFER必须为true,否则无法在 PHP 中控制流式读取 -
CURLOPT_BUFFERSIZE建议设为1024或2048,太小导致频繁回调开销大,太大延迟明显 -
CURLOPT_WRITEFUNCTION是关键:必须自定义回调函数来捕获每一块响应,不能依赖curl_exec()返回值 - OpenAI 的流式响应是
text/event-stream格式,每行以data:开头,末尾带双换行,需手动解析
PHP 8.5 中处理 SSE(Server-Sent Events)响应的字符串解析陷阱
OpenAI 的流式接口返回的不是纯 JSON,而是类 SSE 协议格式:每条消息以 data: 开头,可能跨多行,中间夹着空行,结尾是双换行。直接 json_decode() 会失败,因为传进去的是带前缀的字符串。
使用场景:你拿到一段响应块,比如:"data: {\"id\":\"...\",\"choices\":[{\"delta\":{\"content\":\"h\"}}]}",需要剥离 data: 并只对有效 JSON 部分解码。
立即学习“PHP免费学习笔记(深入)”;
小邮包-包月订购包年服务网,该程序由好买卖商城开发,程序采用PHP+MYSQL架设,程序商业模式为目前最为火爆的包月订制包年服务模式,这种包年订购在国外网站已经热火很多年了,并且已经发展到一定规模,像英国的男士用品网站BlackSocks,一年的袜子购买量更是达到了1000万双。功能:1、实现多产品上线,2、不用注册也可以直接下单购买,3、集成目前主流支付接口,4、下单发货均有邮件提醒。
- 别用
explode("\n", $chunk)后直接json_decode(trim($line))—— 某些 chunk 可能含多行data:或注释行:event - 推荐按行扫描,跳过空行和注释行(以
:开头),提取所有data:行,再对每行substr($line, 6)截取 JSON 部分 - 注意:部分响应可能含
data: [DONE],这是结束信号,不是 JSON,json_last_error()会报错,得先判断 - PHP 8.5 的
str_starts_with()和str_ends_with()可安全用于前缀检测,比strpos() === 0更清晰
PHP 8.5 流式调用中 ob_flush() 和 flush() 不生效的真正原因
即使你正确接收并解析了每个 data: 块,前端仍收不到实时更新?大概率不是代码问题,而是服务器层或 PHP 配置把输出缓存住了。
性能影响:启用输出缓冲后,每次 echo 实际不发包,直到缓冲区满或脚本结束,完全违背流式初衷。
- 确认
output_buffering在php.ini中设为Off(不是0,某些版本0仍启用隐式缓冲) -
zlib.output_compression必须为Off,gzip 压缩会破坏流式数据边界 - Nginx 默认开启
fastcgi_buffering on,需在 location 块中加fastcgi_buffering off; - Apache + mod_php 下,确保没启用
mod_deflate或mod_filter对该路由生效
PHP 8.5 中用 stream_socket_client() 替代 cURL 实现更可控的流式连接
cURL 灵活但抽象层厚,遇到超时重连、连接中断恢复、header 分离等需求时,容易失控。而原生 socket 虽麻烦点,但在 PHP 8.5 中配合 stream_set_timeout() 和 stream_get_line(),反而更稳。
适用条件:你需要精确控制连接生命周期,比如长连接保活、自定义重试逻辑、或绕过 cURL 的 DNS 缓存 bug。
- 构造 HTTP/1.1 请求头必须完整,包括
Host、Authorization、Content-Type和Accept: text/event-stream - 发送完请求头后,用
stream_get_line($fp, 1024, "\r\n\r\n")先读 header,确认HTTP/1.1 200 OK和Content-Type: text/event-stream - 之后循环调用
stream_get_line($fp, 4096, "\n\n")读取每个事件块,比 cURL 的WRITEFUNCTION更易调试 - 注意:socket 连接需手动处理 TLS,用
ssl://api.openai.com:443,且验证证书(stream_context_create(['ssl' => ['verify_peer' => true]]))
fastcgi_buffering 和 PHP 的 output_buffering —— 两者只要一个开着,你在代码里 flush 十次也没用。










