curl_close() 从 PHP 8 开始不写入CURLOPT_COOKIEJAR会话文件 [已关闭]

curl_close() not writing CURLOPT_COOKIEJAR session file from PHP 8 onwards [closed]

提问人:Maurizio 提问时间:10/10/2023 最后编辑:Maurizio 更新时间:10/10/2023 访问量:120

问:


这个问题似乎与帮助中心定义的范围内的编程无关。

上个月关闭。

对于那些遇到会话文件错误的人,其中没有在里面写任何东西(尽管文件存在并且是可写的),使用以下设置:cURLCURLOPT_COOKIEJAR

  1. PHP 8.0 以上
  2. 使用 PHP cURL 发出有状态请求
  3. CURLOPT_COOKIEJAR和/或设置为可读/可写位置CURLOPT_COOKIEFILE
  4. 用于关闭手柄curl_close()

TL;博士;

不要依赖 curl_close()。使用或完成 cURL 请求后。unset($curlHandle)$curlHandle = null

学分:@volkerschulz

为什么会这样

问题与此有关:PHP 的 curl_close() 文档

如文档所述,此函数不起作用。在 PHP 8.0.0 之前,此函数用于关闭资源。curl_close()

具有误导性的是,CURLOPT_COOKIEJAR文档中指出,当句柄关闭时,例如在调用curl_close之后,应填写要保存所有内部 cookie 的文件的名称。CURLOPT_COOKIEJAR

PHP 5.6、7.x 和 8.x 之间的交叉兼容性

为了修补必须保证在多个 PHP 版本上运行的代码,只需遵循以下模式:

$ch = curl_init($url);
...
...
$response = curl_exec($ch);
curl_close($ch);
// always unset curl handle after having closed it.
unset($ch);

可重现的例子

这是重现问题的示例代码。

像这样使用它:

  1. 带补丁/usr/bin/php8.x test.php --with-patch
  2. 没有补丁/usr/bin/php8.x test.php
<?php

// exit if not called from CLI
if (php_sapi_name() !== 'cli') {
    die('This script can only be called from CLI');
}

// exit if the major version of PHP is not 8 or greater
if (substr(phpversion(), 0, 1) < 8) {
    die('This script requires PHP 8 or greater');
}

// exit if we don't have cURL support
if (! function_exists('curl_init')) {
    die('This script requires cURL support');
}

// get the command line option "--with-patch" (if any)
$withPatch = in_array('--with-patch', $argv);
$withPatchClaim = $withPatch ? 'with the unset($ch) patch' : 'without the unset($ch) patch';

// get the major+minor version of PHP
$phpVersion = substr(phpversion(), 0, 3);

// get the version of the cUrl module (if available)
$curlVersion = curl_version()['version'];

echo "Testing with PHP {$phpVersion}, cURL {$curlVersion}. Running {$withPatchClaim}\n";

// define our base path
$basepath = dirname(__FILE__);

// setup a randomic session file for cURL in the current directory, for debugging purposes
$rand = md5(uniqid(rand(), true));
$sessionFile = "{$basepath}/{$rand}.txt";
file_put_contents($sessionFile, '');

// init a curl request to https://dba.stackexchange.com/questions/320782/any-benefit-in-replacing-a-varchar-for-an-index-in-the-main-table
// this url is intended to save cookies in the session file
$url = 'https://dba.stackexchange.com/questions/320782/any-benefit-in-replacing-a-varchar-for-an-index-in-the-main-table';
$ch = curl_init($url);
if (! $ch) {
    die('Could not initialize a cURL session');
}

curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Encoding: utf-8',
    'Accept-Charset: utf-8;q=0.7,*;q=0.7',
    'Cache-Control: no-cache',
    'Connection: Keep-Alive',
    'Referer: https://www.google.com/',
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0',
]);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_NOBODY, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// see https://curl.se/libcurl/c/CURLOPT_COOKIELIST.html
curl_setopt($ch, CURLOPT_COOKIELIST, 'FLUSH');
curl_setopt($ch, CURLOPT_COOKIEJAR, $sessionFile);
// this would not work even with CURLOPT_COOKIEFILE pointing to the same file
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);

// execute the request
$response = curl_exec($ch);
$error = curl_error($ch);
$effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$primaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP);
// as of PHP 8 onwards, the curl_close() call has no effect: https://www.php.net/manual/en/function.curl-close.php
curl_close($ch);

// if we've called this script with "--with-patch", apply the @volkerschulz patch https://stackoverflow.com/a/77261355/1916292
if ($withPatch) {
    unset($ch);
    // the garbage collection is not needed.
    // it is intentionally left here to let the reader understand that the issue is related to destroying the $ch variable
    gc_collect_cycles();
}

// make the response a one-liner
$response = preg_replace('/\s\s+/', ' ', str_replace([chr(13).chr(10), chr(10), chr(9)], ' ', $response));

// print the response (cap to a max of 128 chars)
echo "GET {$effectiveUrl} ({$httpStatusCode})\n";
echo "Primary IP: {$primaryIp}\n";
echo "Error: {$error}\n";
echo 'Response: '.substr($response, 0, 128)." ...\n";

// debug the contents of the session file
echo str_repeat('-', 80)."\n";
echo "Session file: {$sessionFile}\n";

// show the difference from $withPatch and without it
if (! $withPatch) {
    echo "Session file contents without the unset(\$ch) patch (BUGGED):\n";
    echo 'Session Filesize: '.filesize($sessionFile)."\n";
    echo file_get_contents($sessionFile)."\n";
} else {
    echo "Session file contents WITH the unset(\$ch) patch (WORKING):\n";
    echo 'Session Filesize: '.filesize($sessionFile)."\n";
    echo file_get_contents($sessionFile)."\n";
}

启用修补程序的脚本输出:

maurizio:~/test (master ?) $ php8.2 test.php
Testing with PHP 8.2, cURL 7.81.0. Running without the unset($ch) patch
GET https://dba.stackexchange.com/questions/320782/any-benefit-in-replacing-a-varchar-for-an-index-in-the-main-table (200)
Primary IP: 104.18.10.86
Error:
Response: HTTP/2 200 date .... (OMISSIS)
    ----------------------------------------------------------------------
Session file: /home/maurizio/test/100881e90c76fd3a63ee2e6b3b710fea.txt
Session file contents without the unset($ch) patch (BUGGED):
Session Filesize: 0

启用修补程序的脚本输出:

maurizio:~/test (master ?) $ php8.2 test.php --with-patch
Testing with PHP 8.2, cURL 7.81.0. Running with the unset($ch) patch
GET https://dba.stackexchange.com/questions/320782/any-benefit-in-replacing-a-varchar-for-an-index-in-the-main-table (200)
Primary IP: 104.18.10.86
Error:
Response: HTTP/2 200 date .... (OMISSIS)
----------------------------------------------------------------------
Session file: /home/maurizio/test/aa1524b52055bad838f18fae44e83b22.txt
Session file contents WITH the unset($ch) patch (WORKING):
Session Filesize: 431
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_.stackexchange.com    TRUE    /       TRUE    1696928967      __cf_bm OMISSIS
#HttpOnly_.stackexchange.com    TRUE    /       TRUE    1728549567      prov    OMISSIS
卷曲 php-curl

评论

0赞 Barmar 10/10/2023
我刚刚在 PHP 8.2.7 中复制了这一点。
0赞 Barmar 10/10/2023
在我的 Mac 上,我有 PHP 7.4 cURL 8.2.1。但是在我的 Linux 服务器上,我有 PHP 8.2.7 cURL 7.88.1。
1赞 Nigel Ren 10/10/2023
可以指出版本存在一些差异的一件事是来自curl_close手册 - 此功能不起作用。在 PHP 8.0.0 之前,此函数用于关闭资源。
1赞 user1191247 10/10/2023
你有没有试过看看它是否会触发旧行为?unset($ch);curl_close()
2赞 PeeHaa 10/15/2023
我投票关闭这个问题,因为它不是一个问题

答:

3赞 volkerschulz 10/10/2023 #1

从 PHP 8.0.0 开始,不再返回资源,而是返回 CurlHandle 类型的对象。在 PHP 脚本退出后,在垃圾回收器开始操作之前,此对象不会被自动销毁。curl_init()

CURLOPT_COOKIEJAR文档指出:

在句柄关闭时(例如,在调用 curl_close 之后)保存所有内部 cookie 的文件的名称。

当然,最后一部分是误导性的,因为根据其文档,从 PHP 8.0.0 开始不再起作用。curl_close()

如果要强制关闭手柄(即销毁对象),则需要

unset($ch);

或把

$ch = null;

评论

1赞 Maurizio 10/10/2023
谢谢你的贡献。CURLOPT_COOKIEJAR 的文档确实具有误导性,人们可以很容易地将问题与其他问题联系起来,而不是理解从 PHP 8.0 开始的 curl_close() 差异。我会更新我的答案,给出正确的说明和有意义的标题。
0赞 volkerschulz 10/10/2023
有人可能会争辩说,提及只是一个“给出的例子”,仍然适用于较旧的PHP版本,但我100%同意应该在文档中进行澄清。curl_close