Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling Soap timeouts in PHP

1) In case of timeout, PHP throws a SoapFault exception with faultcode="HTTP" and faultstring="Error Fetching http headers".

2) In my opinion, the best way to distinguish between a timeout error and web service issues is by looking at the faultcode and faultstring members of the SoapFault class.
In particular, the faultcode element is intended for use by software to provide an algorithmic mechanism for identifying the fault.
As you can also read in a comment of the PHP manual, there is no method to read the faultcode property, so you have to access it directly (eg. $e->faultcode), because the getCode() method does not work.
The SOAP 1.1 Spec defines four possible values for the faultcode field:

  • VersionMismatch: The processing party found an invalid namespace for the SOAP Envelope element
  • MustUnderstand: An immediate child element of the SOAP Header element that was either not understood or not obeyed by the processing party contained a SOAP mustUnderstand attribute with a value of "1"
  • Client: The Client class of errors indicate that the message was incorrectly formed or did not contain the appropriate information in order to succeed. For example, the message could lack the proper authentication or payment information. It is generally an indication that the message should not be resent without change.
  • Server: The Server class of errors indicate that the message could not be processed for reasons not directly attributable to the contents of the message itself but rather to the processing of the message. For example, processing could include communicating with an upstream processor, which didn't respond. The message may succeed at a later point in time.

In addition to those codes, PHP uses the HTTP code for identifying the errors happening at the protocol level (eg.: socket errors); for example, if you search for add_soap_fault in the ext/soap/php_http.c source code you can see when some of these kind of faults are generated.
By searching for the add_soap_fault and soap_server_fault functions in the PHP SOAP extension source files, I've built the following list of PHP SoapFault exceptions:

HTTP
----
Unable to parse URL
Unknown protocol. Only http and https are allowed.
SSL support is not available in this build
Could not connect to host
Failed Sending HTTP SOAP request
Failed to create stream??
Error Fetching http headers
Error Fetching http body: No Content-Length: connection closed or chunked data
Redirection limit reached: aborting
Didn't recieve an xml document
Unknown Content-Encoding
Can't uncompress compressed response
Error build soap request


VersionMismatch
---------------
Wrong Version


Client
------
A SOAP 1.2 envelope can contain only Header and Body
A SOAP Body element cannot have non Namespace qualified attributes
A SOAP Envelope element cannot have non Namespace qualified attributes
A SOAP Header element cannot have non Namespace qualified attributes
Bad Request
Body must be present in a SOAP envelope
Can't find response data
DTD are not supported by SOAP
encodingStyle cannot be specified on the Body
encodingStyle cannot be specified on the Envelope
encodingStyle cannot be specified on the Header
Error cannot find parameter
Error could not find "location" property
Error finding "uri" property
looks like we got "Body" with several functions call
looks like we got "Body" without function call
looks like we got no XML document
looks like we got XML without "Envelope" element
Missing parameter
mustUnderstand value is not boolean
SoapClient::__doRequest() failed
SoapClient::__doRequest() returned non string value
Unknown Data Encoding Style
Unknown Error
DataEncodingUnknown


MustUnderstand
--------------
Header not understood


Server
------
Couldn't find WSDL
DTD are not supported by SOAP
Unknown SOAP version
WSDL generation is not supported yet

3) To simulate the timeout condition, try with the following code:

soapclient.php

<?php

ini_set('default_socket_timeout', 10);

$client = new SoapClient(null, 
  array(
    'location' => "http://localhost/soapserver.php",
    'uri'      => "http://localhost/soapserver.php",
    'trace'    => 1
  )
);

try {
    echo $return = $client->__soapCall("add",array(41, 51));
} catch (SoapFault $e) {
    echo "<pre>SoapFault: ".print_r($e, true)."</pre>\n";
    //echo "<pre>faultcode: '".$e->faultcode."'</pre>";
    //echo "<pre>faultstring: '".$e->getMessage()."'</pre>";
}

?>

soapserver.php

<?php

function add($a, $b) {
  return $a + $b;
}

sleep(20);

$soap = new SoapServer(null, array('uri' => 'http://localhost/soapserver.php'));
$soap->addFunction("add");
$soap->handle();

?>

Notice the sleep call in the SoapServer.php script with a time (20) longest than the time (10) specified for the default_socket_timeout parameter in the SoapClient.php script.
If you want to simulate a service unavailability, you could for example change the location protocol from http to https in the soapclient.php script, assuming that your web server is not configured for SSL; by doing this, PHP should throw a "Could not connect to host" SoapFault.


Looks like default_socket_timeout is not taken into account when making SOAP calls over HTTPS:

  • Bug #48524 - Timeout setting is not considered on SOAP+HTTPS calls.

Bug open at the time of writing. As a comment on the blog post Robert Ludwick referenced in a deleted answer Timing Out PHP Soap Calls (21 Oct 2009; by Published by Robert F. Ludwick) points out, the workaround the post discusses (overriding SoapClient::__doRequest() with a curl request) works around this bug also.

  • archived copy of <http://www.darqbyte.com/2009/10/21/timing-out-php-soap-calls/>

Another related bug is:

  • Bug #41631 - default_socket_timeout does not work with SSL

The code mentioned in the blog post has undergone some changes and can be found in it's latest form with support of HTTP authentication here on Github:

  • SoapClientTimeout.class.php

In any case, the workaround shouldn't be needed any longer as this problem has been fixed in the PHP SOAPClient extension.


To deal with timeouts in the service

$client = new SoapClient($wsdl, array("connection_timeout"=>10));

// SET SOCKET TIMEOUT
if(defined('RESPONSE_TIMEOUT') &&  RESPONSE_TIMEOUT != '') {
 ini_set('default_socket_timeout', RESPONSE_TIMEOUT);
}

From my experience, if $e->getMessage is "Error Fetching http headers", you are dealing with a network timeout.

If $e->getMessage is something like "Cannot connect to host", the service you are trying to reach is down.

Then there is "Looks like we got no XML document", which is more cryptic an can mean different things.


I used two factors to get my SoapClient extention to throw a nice exception. The message and the time the request took to return. I think the error message "Error Fetching http headers" can also occure in some other cases, therefore the time check.

The following code should be about right

class SoapClientWithTimeout extends SoapClient {
    public function __soapCall ($params, ---) {
        $time_start = microtime(true);
        try {
            $result = parent::__soapCall ($params, ---);
        }
        catch (Exception $e) {
            $time_request = (microtime(true)-$time_start);
            if(
                $e->getMessage() == 'Error Fetching http headers' &&
                ini_get('default_socket_timeout') < $time_request
            ) {
                throw new SoapTimeoutException(
                    'Soap request most likly timed out.'.
                    ' It took '.$time_request.
                    ' and the limit is '.ini_get('default_socket_timeout')
                );
            }

            // E: Not a timeout, let's rethrow the original exception
            throw $e;
        }

        // All good, no exception from the service or PHP
        return $result;
    }
}

class SoapTimeoutException extends Exception {}

I then use SoapClientWithTimeout

$client = new SoapClientWithTimeout();
try {
    $response = $client->SomeSoapRequest();
    var_dump($response);
}
catch(SoapTimeoutException $e){
    echo 'We experienced a timeout! '. $e->getMessage();
}
catch(Exception $e) {
    echo 'Exception: '.$e->getMessage();
}

To debug your service timing out. Add the following line before calling the service

ini_set('default_socket_timeout', 1);