PHP 8 中 fopen 返回的 HTTP stream 为何无法用于 stream_select?
结论
is_resource($stream) 只能确认 $stream 是 PHP stream 资源,无法确认 stream_select() 能否监视它。结合这段代码和 ValueError: No stream arrays were passed,可以先检查 HTTP 响应是否使用了 Transfer-Encoding: chunked。PHP 的 HTTP stream wrapper 可能通过过滤器处理分块响应,而经过过滤的 stream 在当前平台上可能无法转换为 stream_select() 所需的底层句柄。转换失败后,可监视的 stream 集合就会变成空集,从而触发这个错误。
PHP 8 将 HTTP wrapper 的默认 protocol_version 从 1.0 改为 1.1,服务器在 HTTP/1.1 下可能返回分块响应。PHP 的相关问题记录也提到了这种情况下 stream_select() 遇到过滤 stream 的现象。要确认实际原因,还需检查响应头,以及 stream_select() 报错前是否出现警告。PHP stream_select 手册 PHP 问题记录
先检查响应和警告
stream_select() 抛出的 ValueError 之前可能已经出现了其他问题。检查此前是否有类似 Cannot cast a filtered stream on this system 的警告,并查看 HTTP 响应头中是否包含 Transfer-Encoding: chunked。也可以临时启用 PHP 错误日志,以免错误处理配置隐藏前置警告。
资源类型检查依然有用,但它只能检查资源类型和 EOF 状态,无法判断底层句柄是否支持 select。因此,即使下面的检查通过,也不能保证 stream_select() 可以处理该 stream:
is_resource($stream)
&& get_resource_type($stream) === 'stream'
&& !feof($stream)
处理方式
如果只需发送请求并读取响应,不必并发等待多个 stream,可以直接读取响应,不调用 stream_select():
$stream = fopen($url, 'r', false, $context);
if ($stream === false) {
throw new RuntimeException('无法打开 HTTP stream');
}
$response = stream_get_contents($stream);
fclose($stream);
如果确认问题由 HTTP/1.1 分块响应引起,可以尝试将请求协议版本设为 1.0,作为兼容性排查或临时规避办法:
$options = [
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
. 'Content-Length: ' . strlen($content),
'content' => $content,
'timeout' => 30,
'protocol_version' => 1.0,
],
];
$context = stream_context_create($options);
$stream = fopen($url, 'r', false, $context);
这种做法要求服务端接受 HTTP/1.0,并且响应不能让该 stream 进入无法 select 的过滤状态。如果服务器仍返回分块响应,只改请求版本未必能解决问题,因此不要把它当作通用修复。
如果业务需要超时等待或并发管理 HTTP 请求,可以考虑使用支持这些能力的 HTTP 客户端,例如 cURL。不要只凭 is_resource() 就认定 HTTP stream 一定能交给 stream_select()。Insomnia 请求成功只能说明请求本身可能有效,不能证明 PHP 返回的 stream 支持 select。
备注
stream_select() 会修改传入的数组;调用前应按需重新构造 $read、$write 和 $except。排查这个问题时,重点是确认 $read 中是否仍有底层系统能够监视的 stream,而不只是确认数组里曾经放入一个 resource。
备注:内容仅供参考。
