编程 CURLE_SEND_FAIL_REWIND 不是网络问题:PHP 8.6 加了 CURLOPT_SEEKFUNCTION 才补上

2026-10-24 21:08:19

PHP 8.6 的 CURLOPT_SEEKFUNCTION:让流式请求体在重定向时能回绕重发

症状

用 CURLOPT_READFUNCTION 把请求体做成流,往支付网关提交大 JSON 或往对象存储直传文件。多数时候运行正常,但一旦遇到 307/308 重定向,或者连接被复用后 libcurl 决定重试,请求就直接失败,错误码为 CURLE_SEND_FAIL_REWIND,curl_error() 只有一句 “necessary data rewind wasn't possible”。日志里的业务参数看起来没有问题,因为问题不在数据:请求体已经被读走,libcurl 无法把它倒回开头重发。

libcurl 在重定向或重试时必须从请求体开头再读一遍。READFUNCTION 是单向的,读完就没了,也没有任何机制能声明“我可以回绕”,因此 libcurl 只能报错。

以前的三条绕法

  1. 改成字符串 CURLOPT_POSTFIELDS。这种方式最简单,libcurl 自己持有完整 buffer,回绕就不成问题。代价是整个请求体都要进入内存,直传几百 MB 文件不可行,大 JSON 也要计入常驻内存的峰值。

  2. CURLOPT_INFILE + CURLOPT_INFILESIZE(8.5 起有 CURLOPT_INFILESIZE_LARGE,可绕开 32 位有符号整数导致的 2GiB 上限)。它仍然只是单向流,仅换了入口,回绕问题并未解决。

  3. 失败后自己重建 handle 重试。捕获 CURLE_SEND_FAIL_REWIND,关闭 handle,rewind 文件或重新打开流,再发一次。这种做法能跑,但业务层要保证幂等,因为支付类接口重发就是重复下单,而且第一次请求可能已经到达对方。

PHP 8.6 的写法

CURLOPT_SEEKFUNCTION 用于注册一个回调,libcurl 需要回绕时调用它,把流定位到指定 offset。

$path = '/data/upload.bin';
$fh   = fopen($path, 'rb');

$ch = curl_init('https://api.example.com/upload');
curl_setopt_array($ch, [
    CURLOPT_UPLOAD       => true,
    CURLOPT_INFILESIZE   => filesize($path),
    CURLOPT_READFUNCTION => function (CurlHandle $h, $fd, int $len) use ($fh) {
        return fread($fh, $len);
    },
    CURLOPT_SEEKFUNCTION => function (CurlHandle $h, int $offset, int $origin) use ($fh) {
        // $origin 即 fseek 的 whence
        return fseek($fh, $offset, $origin) === 0
            ? CURL_SEEKFUNC_OK
            : CURL_SEEKFUNC_FAIL;
    },
]);

回调签名是 CurlHandle $handle, int $offset, int $origin,返回值三选一:

  • CURL_SEEKFUNC_OK:已定位到 offset,可以继续读。
  • CURL_SEEKFUNC_FAIL:定位失败,本次传输中止。
  • CURL_SEEKFUNC_CANTSEEK:无法定位,但不致命,libcurl 退回到原有行为(继续尝试,最终可能仍是 CURLE_SEND_FAIL_REWIND)。

用 fseek/rewind 最省事;如果请求体是自己拼的字符串或迭代出来的,就在闭包里维护 buffer 和指针,按 offset 切片返回。

这里有个需要注意的问题:返回 CURL_SEEKFUNC_OK 意味着“我已经定位好了”,libcurl 不会再校验。如果回调里算错 offset 却照样返回 OK,后续 READFUNCTION 就会从错误位置读数据,发出去的是错内容,而且是静默的,比直接失败更难查。只有确实定位成功才返回 OK。

验证回绕与连接复用

curl_getinfo() 在 8.6 里新增了 size_delivered 键(也可作 CURLINFO_SIZE_DELIVERED 传入),表示交给下载写回调的总字节数,需要 libcurl 8.20.0 或更高版本。发生重定向后,这个值应与各次响应体字节数之和对应,能用来确认请求体确实被重发过。

判断连接是否真的复用,用 8.5 就有的 CURLINFO_CONN_ID(libcurl >= 8.2.0,数组形式下键为 conn_id):同一连接上的两次传输 ID 相同。配合 8.5 的 curl_share_init_persistent()、CURLOPT_INFILESIZE_LARGE、CURLOPT_FOLLOWLOCATION 取值 CURLFOLLOW_OBEYCODE,可以把“流式上传 + 重定向 + 连接复用”这条路径跑干净。

参考:PHP 8.6 migration – New Features

复制全文 生成海报 PHP cURL 接口对接

推荐文章

程序员茄子在线接单