While I've been working on my new App Engine app and implementing its Twitter integration I came across some bizarre behaviour and a multitude of bugs in the Google cURL Lite implementation when it came to error handling and reporting. Needless to say I did a fair amount of research to see if others have come across the same issue, and they have back in 2015, 3 years ago almost to the date and Google's response was - "We don't claim curl_lite to be a full implementation of cURL".

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.

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!

...so please read on! I love writing articles that provide beneficial information,
tips and examples to my readers. All information on my blog is provided free of
charge and I encourage you to share it as you wish. There is a small favour I ask in return however -
engage in comments below, provide feedback, and if you see mistakes let me know.

If you want to show additional support and help me pay for web hosting and
domain name registration,
donations, no matter how small, are always welcome!

Use of any information contained in this blog post/article is subject to this disclaimer.