Timing Out PHP Soap Calls

Oct 21 2009 Published by under Archive

So I’ve got an interesting technical post for you today. I know I don’t normally post technical things here on my blog, but I felt this was such an interesting exercise in triumphing over a big issue here at work that I wanted to post about it.

We call several third-party vendors for web-based services in my department. We’re really keen on keeping transaction times low, so we have a set timeout for each vendor to ensure we don’t wait too long. Most of our vendor calls are Curl calls, but we have a couple that use Soap.

Curl has built-in functionality to enable a timeout. PHP’s internal Soap library does not. Thus, PHP’s documentation says to enable the following to set a limit on Soap calls:

ini_set("default_socket_timeout", 5); // 5 seconds

This is all fine and dandy. Or, at least, I thought it was. When we first had an outage with one Soap-based vendor, this timeout mechanism worked correctly. However the second time, it did not. We still waited up to 60 seconds for the call despite our code having not changed. Highly distressing is the word.

I researched this issue at great length, and used one of our own Soap services to test out a theory. I put this into the code:

ob_implicit_flush();
echo " ";
sleep(15);

The ob_implicit_flush() function call forces PHP to send any output as it immediately becomes available. Normally, PHP sends it all at the close of the script, or if you use other output buffering functions. Here, I’m forcing some content to be passed back to the caller then sleeping beyond the wait time of 5 seconds.

The results? It waited. So the socket timeout feature in PHP only applies until you receive content. If you receive any content within the socket timeout interval, it will keep the socket open and continue to wait. The timeout actually serves from the opening of the connection to the reception of content, not the entire length of time the socket will remain open.

Thus, I had to find a new route to keeping our Soap calls short. My next attempt was limiting the script execution time via either of these two functions:

set_time_limit(5); // 5 seconds
ini_set("max_execution_time", 5); // 5 seconds

Unfortunately, this didn’t help. First of all, both of these two functions have the exact same effect and use the same underlying PHP functionality. Secondly, they only set limits for internal PHP execution. Any time you have an external data source or blocking system call, this is not calculated in the execution time (at least on Linux; on Windows everything is considered). So any database calls, Soap calls, system calls… These are untimed.

At this point I was at my wits end. I could not figure out a way to limit Soap calls save for building a barebones script to make the Soap call and calling that script with a Curl call.

I ventured into the Soap documentation on the PHP website to see if there was a way I could use the SoapClient class to build the Soap request XML and to parse a Soap response XML into an object, thus allowing me to transport the XML in whatever way I chose. No dice. However, I did discover something interesting while looking at the documentation.

PHP allows you to extend the SoapClient class, I knew that. What I did not know is that you could override certain functions, one of them being __doRequest(). By overriding this function, you can make the request to the remote server however you like.

So I tested this out. And holy crap, it worked. The input to the function is the actual Soap XML, not a Soap object, and the function simply returns the Soap response XML, not an object. It is also passed a few other things, such as the location of the Soap web service. We’re in business.

I built a class extending SoapClient and enabled timeout functionality. When a timeout is used, it actually uses Curl for the call and sets the timeout there. When no timeout is required, it uses the default mechanism to send the request. See part of my class below. It may not be totally robust, but hey, I just needed a timeout. And I couldn’t give you the entire class functionality either; it’s derived from copyrighted code.

class SoapClientTimeout extends SoapClient
{
	private $timeout;
 
	public function __setTimeout($timeout)
	{
		if (!is_int($timeout) && !is_null($timeout))
		{
			throw new Exception("Invalid timeout value");
		}
 
		$this->timeout = $timeout;
	}
 
	public function __doRequest($request, $location, $action, $version, $one_way = FALSE)
	{
		if (!$this->timeout)
		{
			// Call via parent because we require no timeout
			$response = parent::__doRequest($request, $location, $action, $version, $one_way);
		}
		else
		{
			// Call via Curl and use the timeout
			$curl = curl_init($location);
 
			curl_setopt($curl, CURLOPT_VERBOSE, FALSE);
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
			curl_setopt($curl, CURLOPT_POST, TRUE);
			curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
			curl_setopt($curl, CURLOPT_HEADER, FALSE);
			curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: text/xml"));
			curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout);
 
			$response = curl_exec($curl);
 
			if (curl_errno($curl))
			{
				throw new Exception(curl_error($curl));
			}
 
			curl_close($curl);
		}
 
		// Return?
		if (!$one_way)
		{
			return ($response);
		}
	}
}

23 responses so far

  • huypv says:

    Nice post. But when using class SoapClientTimeout, I got error (exception) like that: transfer closed with 391 bytes remaining to read. Can you fix it for me. Thanks!

    • Robert says:

      You could try pushing your timeout a little longer to accommodate… See the whole point of using this class instead of the socket timeout is that if it isn’t done in a certain amount of time, it stops. With the socket timeout, if it starts within the timeout period, it’s allowed to finish.

      Do you get that error every time, regardless of the timeout length?

  • huypv says:

    - Do you get that error every time, regardless of the timeout length?
    - Yes!

    I checked log on Soap Server, request from Soap Client is already executed successfully. :(

  • [...] what happened!” soon, but who knows. Occasionally I like to have posts like my PHP Soap Timeout post, so maybe I’ll come up with something awesome like that soon. But for now, all you get [...]

  • [...] SF might be able to increase the 60 threshold but I don't think this is the best approach Things I would like to test: Force the socket closed:Timing Out PHP Soap Calls | DarqByte [...]

  • Dan says:

    Thanks so much for this – this solved a problem I was having with a bug in php where timeouts don’t work for https calls.

  • GR says:

    Perfect solution

    huypv – For error I’ve used “try{}catch (Exception $e) {$e->getMessage();}

    So I can catch error and continue my own php code.

    Thanks alot to Robert

  • John Ennew says:

    Hi, thanks for this – I can confirm it works for me.

    I had to make a slight improvement when the API changed for the Soap Service I was using. They required a SOAPAction header which the code here does not provide.

    Replace the line above:
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(“Content-Type: text/xml”));

    With:
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(“Content-Type: text/xml”, “SOAPAction: {$action}”));

    Before proiding the SOAPAction I was getting an error message about contract mismatch between client and server.

  • Robert says:

    @John Ennew
    Ah, that makes sense. In the SOAP services I was consuming, I didn’t have to send that along to get it to work properly, but that’s a more robust solution. Thanks for the addition!

  • [...] Thanks to Rob Ludwick for providing a workaround using CURL as the transport mechanism instead until PHP fix this. You can read about it on his blog here: http://www.darqbyte.com/2009/10/21/timing-out-php-soap-calls [...]

  • Pablo Cesar says:

    Perfect! just got an error with SSL3_GET_SERVER_CERTIFICATE using https and fixed with

    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER,FALSE);

    Thanks a lot, this was driving me crazy.

  • Garvin says:

    SoapClient is a wonderful tool for building requests and parsing responses, but its networking and debugging need improvement. Many times I thought: “I wish I could use cURL instead of SoapClient”, but I didn’t want to reinvent the request/response wheel. So thank you for thinking up the solution and publishing this.

  • Timothy J says:

    Can someone provide an example of how do use this class?

    I have included this class to extend the SoapClient, but not sure where I set $timeout?

    My code:

    $client = new SoapClientTimeout(“*****/webService?wsdl”);
    $response = $client->getUserXml($lookupID);
    $xml = new SimpleXMLElement($response);

    I am able to use my $client object, but would really like to know how I can call the __setTimeout function.

    Thanks in advance

  • Robert says:

    @Timothy J

    Not too hard, really. Example:

    $client = new SoapClientTimeout(“*****/webService?wsdl”);
    $client->__setTimeout(5);
    $response = $client->getUserXml($lookupID);
    $xml = new SimpleXMLElement($response);

  • Timothy J says:

    Robert :
    @Timothy J
    Not too hard, really. Example:
    $client = new SoapClientTimeout(“*****/webService?wsdl”);
    $client->__setTimeout(5);
    $response = $client->getUserXml($lookupID);
    $xml = new SimpleXMLElement($response);

    Hi Robert,

    Thank you for the timely reply.

    The __setTimeout is not throwing any errors for me. In testing the above code, I took my WebService offline and the page will resolves to a “The connection was reset.” And this causes and Application Error in my PHP logs.

    It is to my understanding that the __setTimeout(x) will throw an error when the WebService is unreachable?

    - Tim

  • Robert says:

    @Timothy J

    Tim, the __setTimeout() function is just to internally set the amount of time you want to wait when you do eventually run getUserXml(). __setTimeout() will only throw an exception if you pass it a bad value. The exception will be thrown inside __doRequest(), which is automatically called from getUserXml() and any other SOAP-defined function that the client can run.

    Offhand, I’m not sure why the SOAP client isn’t timing out properly, unless the client is disabled along with the webservice.

  • Styxit says:

    Login through http is not supported in the original version, because Curl does not use the SOAP Authorization. See my Fork at https://gist.github.com/3169626 If you require this. This sets CURLOPT_USERPWD and CURLOPT_HTTPAUTH to use your login and password details as provided in the soap-options.

  • Great solution! But isn’t there a global variable for SOAP Calls (not SOAP connections), I’m experiencing timeouts of exactly 1 minute.

Leave a Reply