- URL: https://www.laruence.com/en/2014/01/21/2939.html
- Please include attribution when republishing.
Recently our services were upgrading the libcurl that PHP uses, hoping the newer libcurl would support millisecond-level timeouts so we could control backend API timeouts more finely, and thus improve overall response time.
But we found that on our CentOS servers, once you set a timeout below 1000ms, curl wouldn't make any request at all — it would just return a timeout error (Timeout reached 28).
It turns out there's a gotcha here. By default, on Linux systems, CURL uses SIGALARM to provide the DNS-resolution timeout control when it relies on the system's standard DNS resolution. But SIGALARM doesn't support timeouts below 1s. So in the libcurl 7.28.1 source (note the Chinese comment lines):
int Curl_resolv_timeout(struct connectdata *conn,
const char *hostname,
int port,
struct Curl_dns_entry **entry,
long timeoutms)
{
.......
.......
#ifdef USE_ALARM_TIMEOUT
if(data->set.no_signal)
/* Ignore the timeout when signals are disabled */
timeout = 0;
else
timeout = timeoutms;
if(!timeout)
/* USE_ALARM_TIMEOUT defined, but no timeout actually requested */
return Curl_resolv(conn, hostname, port, entry);
if(timeout < 1000) //if less than 1000, time out and return immediately
/* The alarm() function only provides integer second resolution, so if
we want to wait less than one second we must bail out already now. */
return CURLRESOLV_TIMEDOUT;
....
....
As you can see, when your timeout is below 1000ms, name resolution returns CURLRESOLV_TIMEOUT directly, which ultimately leads to CURLE_OPERATION_TIMEDOUT, and then Error, Timeout reached...
This... is way too much of a trap, isn't it? Are you telling me we simply can't use millisecond timeouts? Then why did you provide this feature at all?
Let's look at the code again — the same snippet as before, and note this (the Chinese comment line):
#ifdef USE_ALARM_TIMEOUT
if(data->set.no_signal) //note this line
/* Ignore the timeout when signals are disabled */
timeout = 0;
else
timeout = timeoutms;
if(!timeout)
/* USE_ALARM_TIMEOUT defined, but no timeout actually requested */
return Curl_resolv(conn, hostname, port, entry);
if(timeout < 1000)
/* The alarm() function only provides integer second resolution, so if
we want to wait less than one second we must bail out already now. */
return CURLRESOLV_TIMEDOUT;
It looks like as long as this set.no_signal thing is 1, we can bypass it... So what is this thing?
That part is simple — grep the code and you'll find:
case CURLOPT_NOSIGNAL:
/*
* The application asks not to set any signal() or alarm() handlers,
* even when using a timeout.
*/
data->set.no_signal = (0 != va_arg(param, long))?TRUE:FALSE;
break;
Haha, so it's this thing:
<?php curl_setopt($ch, CURLOPT_NOSIGNAL, 1); ?>
After adding this OPT, everything finally works!
Postscript:
This way, though, there's a hidden risk: DNS resolution will no longer be subject to the timeout. Within a company's internal network this usually isn't a problem, but if the DNS server ever hangs, it could cause the application to time out.
So is there another way?
Yes — as Mike pointed out, we can have libcurl use c-ares (a C library for asynchronous DNS requests) to do the name resolution. Specifically, when you configure curl:
./configure --enable-ares[=PATH]
That way you don't need to set NOSIGNAL at all 🙂
PS: why call it a "Bug"? I'm just curious — why don't they use setitimer?
Reference: http://stackoverflow.com/questions/7987584/curl-timeout-less-than-1000ms-always-fails
Be First to Comment