There doesn't appear to have been any update to the PHP runtime files in github since that time, which I find surprising. cURL Lite is part of the runtime. Google's response also didn't sit well with me since the issues with cURL Lite were not about being a full implementation but obvious bugs that needed to be addressed.
So instead of writing a workaround and getting on with my own code I decided to see what was going on with cURL Lite. The main file under scrutiny here is CurlLite.php. This file resides in the google-cloud-sdk/platform/google_appengine/php/sdk/google/appengine/runtime directory after installing the Cloud SDK. There are other files involved here like CurlLiteStub.php but that's mostly to provide the curl_* functions to the PHP runtime and map them to cURL Lite so I won't be touching on that here.
The first issue I will address is the fact that curl_error() always returns a value even if everything was successful (or not even executed). The following bit of code shows this...
PHP
$ch = curl_init();
error_log('cURL Error: [' . curl_error($ch) . ']')
curl_close($ch);
In in App Engine environment this will come back as...
Output
cURL Error: [OK]
Non-App Engine PHP environments and the real cURL extension will produce the following output instead...
Output
cURL Error: []
This massively breaks error handling in places that the actual error string is being compared. This was the case in the Twitter library I was using. I would say this approach to error checking is bad practice and instead curl_errno() should be used, which works correctly in GAE. Still, cURL Lite should not introduce such a code breaking change in the behaviour of an essential function.
The above is caused by this line in CurlLite.php:179...
PHP
private $error_string = "OK";
Whoops! It's an oversight and I've made worse mistakes and this one I can let slide. So moving on!
The next issue I found was the cause of cURL Lite always returning "No URL set!" as an error, even on successful fetches. This one took more time to track down and was due to a bug and some broken logic in setOption() in CurlLite.php.
The following code reproduces the bug...
PHP
$ch = curl_init();
$options = [
CURLOPT_URL => 'http://www.example.com/',
CURLOPT_HEADER => false
];
curl_setopt_array($ch, $options);
error_log('cURL Error: [' . curl_error($ch) . ']');
error_log('cURL Error #: [' . curl_errno($ch) . ']');
curl_close($ch);
In App Engine the output is -
Output
cURL Error: [No URL set!]
cURL Error #: [3]
Whereas non-App Engine PHP environments and the real cURL extension produce -
Output
cURL Error: []
cURL Error #: [0]
Boo. I set the URL! So why do you lie to me cURL Lite!?
Well the error code and message are due to setRequestUrl() function in CurlLite.php, but it's not the culprit, it's just the messenger!
PHP
private function setRequestUrl() {
if ($this->tryGetOption(CURLOPT_URL, $value) && $value) {
if (static::isSupportedUrlScheme($value, $scheme)) {
$this->request->setUrl($value);
return true;
} else {
$this->setError(CURLE_UNSUPPORTED_PROTOCOL,
sprintf("Unsupported protocol '%s'", $scheme));
}
} else {
$this->setError(CURLE_URL_MALFORMAT, "No URL set!");
}
return false;
}
There's nothing wrong with the code above and it's clear how we can get to the condition where the "No URL set!" error gets set. Though I am not a huge fan of the way tryGetOption() is called - I'd have declared $value as false prior myself.
Now to the real bug. Note that setRequestUrl() does not accept any function parameters, instead it uses tryGetOption() to fetch the value of the URL. Now if we go over to setOption(), which is called when curl_setopt_array() is called, it enters a switch statement right away and then we get to this code...
PHP
case CURLOPT_URL:
$this->setRequestUrl($value);
break;
What is this? The $value is being passed to setRequestUrl() but it doesn't accept it! Obvious bug is obvious. In itself this doesn't cause setRequestUrl() to fail, but there's an additional logic error that does!
Right after that switch statement I just discussed there's this line...
PHP
$this->options[$key] = $value;
This sets the value of the URL on the correct CURLOPT_URL index that setRequestUrl() looks for, but since it's set after the call to setRequestUrl() it's too late and by that point the error message and error code are already set and the cURL session error state has been poisoned. Since the error state is not reset during a session, cURL Lite always ends up returning an error.
Now there is a way around this. The trick is to check for the return value of curl_exec(). If the value is set to false, there was an error, otherwise we're all good. The identical operator === must be used for this check.
Even though there are bugs and inconsistencies to the real cURL implementation in cURL Lite, it's still a useful feature to use. Just be careful when doing so!
-i