SoapFault handling refactored: client now returns server fault codes + more details in message

This commit is contained in:
Petr Bechyně 2017-05-26 10:53:41 +02:00
parent f669c18c7f
commit 8db9b374e4
7 changed files with 361 additions and 188 deletions

View File

@ -152,10 +152,11 @@ class Curl
preg_match('/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $executeSoapCallResponse, $httpResponseMessages); preg_match('/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $executeSoapCallResponse, $httpResponseMessages);
$httpResponseMessage = trim(array_pop($httpResponseMessages)); $httpResponseMessage = trim(array_pop($httpResponseMessages));
$curlErrorMessage = sprintf( $curlErrorMessage = sprintf(
'Curl error "%s" with message: %s occurred while connecting to %s', 'Curl error "%s" with message: %s occurred while connecting to %s with HTTP response code %s',
curl_errno($curlSession), curl_errno($curlSession),
curl_error($curlSession), curl_error($curlSession),
$location $location,
$httpResponseCode
); );
if (!is_integer($httpResponseCode) || $httpResponseCode >= 400 || $httpResponseCode === 0) { if (!is_integer($httpResponseCode) || $httpResponseCode >= 400 || $httpResponseCode === 0) {

View File

@ -19,6 +19,7 @@ use BeSimple\SoapClient\Curl\CurlOptionsBuilder;
use BeSimple\SoapClient\Curl\CurlResponse; use BeSimple\SoapClient\Curl\CurlResponse;
use BeSimple\SoapClient\SoapOptions\SoapClientOptions; use BeSimple\SoapClient\SoapOptions\SoapClientOptions;
use BeSimple\SoapCommon\Fault\SoapFaultEnum; use BeSimple\SoapCommon\Fault\SoapFaultEnum;
use BeSimple\SoapCommon\Fault\SoapFaultParser;
use BeSimple\SoapCommon\Fault\SoapFaultPrefixEnum; use BeSimple\SoapCommon\Fault\SoapFaultPrefixEnum;
use BeSimple\SoapCommon\Fault\SoapFaultSourceGetter; use BeSimple\SoapCommon\Fault\SoapFaultSourceGetter;
use BeSimple\SoapCommon\Mime\PartFactory; use BeSimple\SoapCommon\Mime\PartFactory;
@ -40,16 +41,14 @@ use SoapFault;
*/ */
class SoapClient extends \SoapClient class SoapClient extends \SoapClient
{ {
use SoapClientNativeMethodsTrait;
/** @var SoapClientOptions */ /** @var SoapClientOptions */
protected $soapClientOptions; protected $soapClientOptions;
/** @var SoapOptions */ /** @var SoapOptions */
protected $soapOptions; protected $soapOptions;
/** @var Curl */ /** @var Curl */
private $curl; private $curl;
/** @var SoapAttachment[] */
private $soapAttachmentsOnRequestStorage;
/** @var SoapResponse */
private $soapResponseStorage;
public function __construct(SoapClientOptions $soapClientOptions, SoapOptions $soapOptions) public function __construct(SoapClientOptions $soapClientOptions, SoapOptions $soapOptions)
{ {
@ -76,33 +75,6 @@ class SoapClient extends \SoapClient
@parent::__construct($wsdlPath, $soapClientOptions->toArray() + $soapOptions->toArray()); @parent::__construct($wsdlPath, $soapClientOptions->toArray() + $soapOptions->toArray());
} }
/**
* Avoid using __call directly, it's deprecated even in \SoapClient.
*
* @deprecated
*/
public function __call($function_name, $arguments)
{
throw new Exception(
'The __call method is deprecated. Use __soapCall/soapCall instead.'
);
}
/**
* Using __soapCall returns only response string, use soapCall instead.
*
* @param string $function_name
* @param array $arguments
* @param array|null $options
* @param null $input_headers
* @param array|null $output_headers
* @return string
*/
public function __soapCall($function_name, $arguments, $options = null, $input_headers = null, &$output_headers = null)
{
return $this->soapCall($function_name, $arguments, $options, $input_headers, $output_headers)->getResponseContent();
}
/** /**
* @param string $functionName * @param string $functionName
* @param array $arguments * @param array $arguments
@ -125,95 +97,13 @@ class SoapClient extends \SoapClient
} catch (SoapFault $soapFault) { } catch (SoapFault $soapFault) {
if (SoapFaultSourceGetter::isNativeSoapFault($soapFault)) { if (SoapFaultSourceGetter::isNativeSoapFault($soapFault)) {
$soapResponse = $this->getSoapResponseFromStorage(); $soapFault = $this->decorateNativeSoapFault($soapFault);
if ($soapResponse instanceof SoapResponse) {
$soapFault = $this->throwSoapFaultByTracing(
SoapFaultPrefixEnum::PREFIX_PHP . '-' . $soapFault->getCode(),
$soapFault->getMessage(),
new SoapResponseTracingData(
'Content-Type: ' . $soapResponse->getRequest()->getContentType(),
$soapResponse->getRequest()->getContent(),
'Content-Type: ' . $soapResponse->getContentType(),
$soapResponse->getResponseContent()
)
);
} else {
$soapFault = new SoapFault(
SoapFaultPrefixEnum::PREFIX_PHP . '-unresolved',
'Got SoapFault message with no response: '.$soapFault->getMessage()
);
}
} }
throw $soapFault; throw $soapFault;
} }
} }
/**
* This is not performing any HTTP requests, but it is getting data from SoapClient that are needed for this Client
*
* @param string $request Request string
* @param string $location Location
* @param string $action SOAP action
* @param int $version SOAP version
* @param int $oneWay 0|1
*
* @return string
*/
public function __doRequest($request, $location, $action, $version, $oneWay = 0)
{
$soapResponse = $this->performSoapRequest(
$request,
$location,
$action,
$version,
$this->getSoapAttachmentsOnRequestFromStorage()
);
$this->setSoapResponseToStorage($soapResponse);
return $soapResponse->getResponseContent();
}
/** @deprecated */
public function __getLastRequestHeaders()
{
$this->checkTracing();
throw new Exception(
'The __getLastRequestHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** @deprecated */
public function __getLastRequest()
{
$this->checkTracing();
throw new Exception(
'The __getLastRequest method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** @deprecated */
public function __getLastResponseHeaders()
{
$this->checkTracing();
throw new Exception(
'The __getLastResponseHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** @deprecated */
public function __getLastResponse()
{
$this->checkTracing();
throw new Exception(
'The __getLastResponse method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** /**
* Custom request method to be able to modify the SOAP messages. * Custom request method to be able to modify the SOAP messages.
* $oneWay parameter is not used at the moment. * $oneWay parameter is not used at the moment.
@ -226,7 +116,7 @@ class SoapClient extends \SoapClient
* *
* @return SoapResponse * @return SoapResponse
*/ */
private function performSoapRequest($request, $location, $action, $version, array $soapAttachments = []) protected function performSoapRequest($request, $location, $action, $version, array $soapAttachments = [])
{ {
$soapRequest = $this->createSoapRequest($location, $action, $version, $request, $soapAttachments); $soapRequest = $this->createSoapRequest($location, $action, $version, $request, $soapAttachments);
@ -273,22 +163,10 @@ class SoapClient extends \SoapClient
*/ */
private function performHttpSoapRequest(SoapRequest $soapRequest) private function performHttpSoapRequest(SoapRequest $soapRequest)
{ {
if ($soapRequest->getVersion() === SOAP_1_1) {
$headers = [
'Content-Type: ' . $soapRequest->getContentType(),
'SOAPAction: "' . $soapRequest->getAction() . '"',
'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'),
];
} else {
$headers = [
'Content-Type: ' . $soapRequest->getContentType() . '; action="' . $soapRequest->getAction() . '"',
'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'),
];
}
$curlResponse = $this->curl->executeCurlWithCachedSession( $curlResponse = $this->curl->executeCurlWithCachedSession(
$soapRequest->getLocation(), $soapRequest->getLocation(),
$soapRequest->getContent(), $soapRequest->getContent(),
$headers $this->getHttpHeadersBySoapVersion($soapRequest)
); );
$soapResponseTracingData = new SoapResponseTracingData( $soapResponseTracingData = new SoapResponseTracingData(
$curlResponse->getHttpRequestHeaders(), $curlResponse->getHttpRequestHeaders(),
@ -310,26 +188,42 @@ class SoapClient extends \SoapClient
$this->getAttachmentFilters(), $this->getAttachmentFilters(),
$this->soapOptions->getAttachmentType() $this->soapOptions->getAttachmentType()
); );
} else {
return $soapResponse;
} }
} else if ($curlResponse->curlStatusFailed()) {
return $soapResponse;
}
if ($curlResponse->curlStatusFailed()) {
if ($curlResponse->getHttpResponseStatusCode() >= 500) {
$soapFault = SoapFaultParser::parseSoapFault(
$curlResponse->getResponseBody()
);
return $this->throwSoapFaultByTracing(
$soapFault->faultcode,
sprintf(
'SOAP HTTP call failed: %s with Message: %s and Code: %s',
$curlResponse->getCurlErrorMessage(),
$soapFault->getMessage(),
$soapFault->faultcode
),
$soapResponseTracingData
);
}
return $this->throwSoapFaultByTracing( return $this->throwSoapFaultByTracing(
SoapFaultEnum::SOAP_FAULT_HTTP.'-'.$curlResponse->getHttpResponseStatusCode(), SoapFaultEnum::SOAP_FAULT_HTTP.'-'.$curlResponse->getHttpResponseStatusCode(),
$curlResponse->getCurlErrorMessage(), $curlResponse->getCurlErrorMessage(),
$soapResponseTracingData $soapResponseTracingData
); );
} else {
return $this->throwSoapFaultByTracing(
SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR,
'Cannot process curl response with unresolved status: ' . $curlResponse->getCurlStatus(),
$soapResponseTracingData
);
} }
return $this->throwSoapFaultByTracing(
SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR,
'Cannot process curl response with unresolved status: ' . $curlResponse->getCurlStatus(),
$soapResponseTracingData
);
} }
/** /**
@ -383,18 +277,16 @@ class SoapClient extends \SoapClient
$soapResponseTracingData, $soapResponseTracingData,
$soapAttachments $soapAttachments
); );
} else {
return SoapResponseFactory::create(
$curlResponse->getResponseBody(),
$soapRequest->getLocation(),
$soapRequest->getAction(),
$soapRequest->getVersion(),
$curlResponse->getHttpResponseContentType(),
$soapAttachments
);
} }
return SoapResponseFactory::create(
$curlResponse->getResponseBody(),
$soapRequest->getLocation(),
$soapRequest->getAction(),
$soapRequest->getVersion(),
$curlResponse->getHttpResponseContentType(),
$soapAttachments
);
} }
/** /**
@ -412,49 +304,59 @@ class SoapClient extends \SoapClient
$soapFaultMessage, $soapFaultMessage,
$soapResponseTracingData $soapResponseTracingData
); );
}
throw new SoapFault(
$soapFaultCode,
$soapFaultMessage
);
}
private function decorateNativeSoapFault(SoapFault $nativePhpSoapFault)
{
$soapResponse = $this->getSoapResponseFromStorage();
if ($soapResponse instanceof SoapResponse) {
$tracingData = new SoapResponseTracingData(
'Content-Type: ' . $soapResponse->getRequest()->getContentType(),
$soapResponse->getRequest()->getContent(),
'Content-Type: ' . $soapResponse->getContentType(),
$soapResponse->getResponseContent()
);
$soapFault = $this->throwSoapFaultByTracing(
SoapFaultPrefixEnum::PREFIX_PHP . '-' . $nativePhpSoapFault->getCode(),
$nativePhpSoapFault->getMessage(),
$tracingData
);
} else { } else {
$soapFault = $this->throwSoapFaultByTracing(
throw new SoapFault( $nativePhpSoapFault->faultcode,
$soapFaultCode, $nativePhpSoapFault->getMessage(),
$soapFaultMessage new SoapResponseTracingData(
null,
null,
null,
null
)
); );
} }
return $soapFault;
} }
private function checkTracing() private function getHttpHeadersBySoapVersion(SoapRequest $soapRequest)
{ {
if ($this->soapClientOptions->getTrace() === false) { if ($soapRequest->getVersion() === SOAP_1_1) {
throw new Exception('SoapClientOptions tracing disabled, turn on trace attribute');
return [
'Content-Type: ' . $soapRequest->getContentType(),
'SOAPAction: "' . $soapRequest->getAction() . '"',
'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'),
];
} }
}
private function setSoapResponseToStorage(SoapResponse $soapResponseStorage) return [
{ 'Content-Type: ' . $soapRequest->getContentType() . '; action="' . $soapRequest->getAction() . '"',
$this->soapResponseStorage = $soapResponseStorage; 'Connection: ' . ($this->soapOptions->isConnectionKeepAlive() ? 'Keep-Alive' : 'close'),
} ];
/**
* @param SoapAttachment[] $soapAttachments
*/
private function setSoapAttachmentsOnRequestToStorage(array $soapAttachments)
{
$this->soapAttachmentsOnRequestStorage = $soapAttachments;
}
private function getSoapAttachmentsOnRequestFromStorage()
{
$soapAttachmentsOnRequest = $this->soapAttachmentsOnRequestStorage;
$this->soapAttachmentsOnRequestStorage = null;
return $soapAttachmentsOnRequest;
}
private function getSoapResponseFromStorage()
{
$soapResponse = $this->soapResponseStorage;
$this->soapResponseStorage = null;
return $soapResponse;
} }
} }

View File

@ -0,0 +1,166 @@
<?php
namespace BeSimple\SoapClient;
use BeSimple\SoapBundle\Soap\SoapAttachment;
use BeSimple\SoapClient\SoapOptions\SoapClientOptions;
use Exception;
trait SoapClientNativeMethodsTrait
{
/** @var SoapClientOptions */
protected $soapClientOptions;
/** @var SoapAttachment[] */
private $soapAttachmentsOnRequestStorage;
/** @var SoapResponse */
private $soapResponseStorage;
/**
* @param string $functionName
* @param array $arguments
* @param array|null $options
* @param SoapAttachment[] $soapAttachments
* @param null $inputHeaders
* @param array|null $outputHeaders
* @return SoapResponse
*/
abstract public function soapCall($functionName, array $arguments, array $soapAttachments = [], array $options = null, $inputHeaders = null, array &$outputHeaders = null);
/**
* @param mixed $request Request object
* @param string $location Location
* @param string $action SOAP action
* @param int $version SOAP version
* @param SoapAttachment[] $soapAttachments SOAP attachments array
* @return SoapResponse
*/
abstract protected function performSoapRequest($request, $location, $action, $version, array $soapAttachments = []);
/**
* Avoid using __call directly, it's deprecated even in \SoapClient.
*
* @deprecated
*/
public function __call($function_name, $arguments)
{
throw new Exception(
'The __call method is deprecated. Use __soapCall/soapCall instead.'
);
}
/**
* Using __soapCall returns only response string, use soapCall instead.
*
* @param string $function_name
* @param array $arguments
* @param array|null $options
* @param null $input_headers
* @param array|null $output_headers
* @return string
*/
public function __soapCall($function_name, $arguments, $options = null, $input_headers = null, &$output_headers = null)
{
return $this->soapCall($function_name, $arguments, $options, $input_headers, $output_headers)->getResponseContent();
}
/**
* This is not performing any HTTP requests, but it is getting data from SoapClient that are needed for this Client
*
* @param string $request Request string
* @param string $location Location
* @param string $action SOAP action
* @param int $version SOAP version
* @param int $oneWay 0|1
*
* @return string
*/
public function __doRequest($request, $location, $action, $version, $oneWay = 0)
{
$soapResponse = $this->performSoapRequest(
$request,
$location,
$action,
$version,
$this->getSoapAttachmentsOnRequestFromStorage()
);
$this->setSoapResponseToStorage($soapResponse);
return $soapResponse->getResponseContent();
}
/** @deprecated */
public function __getLastRequestHeaders()
{
$this->checkTracing();
throw new Exception(
'The __getLastRequestHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** @deprecated */
public function __getLastRequest()
{
$this->checkTracing();
throw new Exception(
'The __getLastRequest method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** @deprecated */
public function __getLastResponseHeaders()
{
$this->checkTracing();
throw new Exception(
'The __getLastResponseHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
/** @deprecated */
public function __getLastResponse()
{
$this->checkTracing();
throw new Exception(
'The __getLastResponse method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.'
);
}
private function checkTracing()
{
if ($this->soapClientOptions->getTrace() === false) {
throw new Exception('SoapClientOptions tracing disabled, turn on trace attribute');
}
}
private function setSoapResponseToStorage(SoapResponse $soapResponseStorage)
{
$this->soapResponseStorage = $soapResponseStorage;
}
/**
* @param SoapAttachment[] $soapAttachments
*/
private function setSoapAttachmentsOnRequestToStorage(array $soapAttachments)
{
$this->soapAttachmentsOnRequestStorage = $soapAttachments;
}
private function getSoapAttachmentsOnRequestFromStorage()
{
$soapAttachmentsOnRequest = $this->soapAttachmentsOnRequestStorage;
$this->soapAttachmentsOnRequestStorage = null;
return $soapAttachmentsOnRequest;
}
private function getSoapResponseFromStorage()
{
$soapResponse = $this->soapResponseStorage;
$this->soapResponseStorage = null;
return $soapResponse;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace BeSimple\SoapCommon\Fault;
use SimpleXMLElement;
use SoapFault;
class SoapFaultParser
{
/**
* @param string $soapFaultXmlSource
* @return SoapFault
*/
public static function parseSoapFault($soapFaultXmlSource)
{
$simpleXMLElement = new SimpleXMLElement($soapFaultXmlSource);
$faultCode = $simpleXMLElement->xpath('//faultcode');
if ($faultCode === false || count($faultCode) === 0) {
$faultCode = 'Unable to parse faultCode';
}
$faultString = $simpleXMLElement->xpath('//faultstring');
if ($faultString === false || count($faultString) === 0) {
$faultString = 'Unable to parse faultString';
}
return new SoapFault(
(string)$faultCode[0],
(string)$faultString[0]
);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace BeSimple\SoapCommon\Fault;
use PHPUnit_Framework_TestCase;
use SoapFault;
class SoapFaultParserTest extends PHPUnit_Framework_TestCase
{
public function testParse()
{
$soapFaultXml = '<?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>911</faultcode><faultstring>This is a dummy SoapFault.</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>';
$soapFault = SoapFaultParser::parseSoapFault($soapFaultXml);
self::assertInstanceOf(SoapFault::class, $soapFault);
self::assertEquals(
'911',
$soapFault->faultcode
);
self::assertEquals(
'This is a dummy SoapFault.',
$soapFault->getMessage()
);
}
}

View File

@ -17,6 +17,7 @@ use Fixtures\DummyServiceMethodWithIncomingLargeSwaRequest;
use Fixtures\DummyServiceMethodWithOutgoingLargeSwaRequest; use Fixtures\DummyServiceMethodWithOutgoingLargeSwaRequest;
use Fixtures\GenerateTestRequest; use Fixtures\GenerateTestRequest;
use PHPUnit_Framework_TestCase; use PHPUnit_Framework_TestCase;
use SoapFault;
use SoapHeader; use SoapHeader;
class SoapServerAndSoapClientCommunicationTest extends PHPUnit_Framework_TestCase class SoapServerAndSoapClientCommunicationTest extends PHPUnit_Framework_TestCase
@ -124,6 +125,44 @@ class SoapServerAndSoapClientCommunicationTest extends PHPUnit_Framework_TestCas
); );
} }
public function testSoapCallSwaWithLargeSwaResponseWithSoapFault()
{
$soapClient = $this->getSoapBuilder()->buildWithSoapHeader(
SoapClientOptionsBuilder::createWithEndpointLocation(
self::TEST_HTTP_URL.'/SwaSenderSoapFaultEndpoint.php'
),
SoapOptionsBuilder::createSwaWithClassMap(
self::TEST_HTTP_URL.'/SwaSenderEndpoint.php?wsdl',
new ClassMap([
'GenerateTestRequest' => GenerateTestRequest::class,
]),
SoapOptions::SOAP_CACHE_TYPE_NONE
),
new SoapHeader('http://schema.testcase', 'SoapHeader', [
'user' => 'admin',
])
);
$this->setExpectedException(SoapFault::class);
try {
$soapClient->soapCall('dummyServiceMethodWithOutgoingLargeSwa', []);
} catch (SoapFault $e) {
self::assertEquals(
'911',
$e->faultcode
);
self::assertEquals(
'SOAP HTTP call failed: Curl error "0" with message: occurred while connecting to http://localhost:8000/tests/SwaSenderSoapFaultEndpoint.php with HTTP response code 500 with Message: This is a dummy SoapFault. and Code: 911',
$e->getMessage()
);
throw $e;
}
self::fail('Expected SoapFault was not thrown');
}
public function testSoapCallWithLargeSwaRequest() public function testSoapCallWithLargeSwaRequest()
{ {
$soapClient = $this->getSoapBuilder()->buildWithSoapHeader( $soapClient = $this->getSoapBuilder()->buildWithSoapHeader(

View File

@ -0,0 +1,9 @@
<?php
const FIXTURES_DIR = __DIR__.'/Fixtures';
$soapServer = new \SoapServer(FIXTURES_DIR.'/DummyService.wsdl');
$soapServer->fault(
911,
'This is a dummy SoapFault.'
);