From 6f31a50d63a22fe9b831e8d170f29bfe291b17bc Mon Sep 17 00:00:00 2001 From: Gasol Wu Date: Wed, 12 Dec 2018 16:32:54 +0800 Subject: [PATCH] Reduce memory footprint when parsing HTTP result In order to resolve #82, Remove the substr function in `parseCurlResult()` and register receiveCurlHeader as callback function by using CURLOPT_HEADERFUNCTION instead, To avoid additional memory copy of response body (from $response to $responseBody). This changes is not intended to resolve issue #15 --- lib/Client.php | 53 ++-- tests/HTTP/ClientTest.php | 41 ++- tests/HTTP/ClientTest.php.orig | 551 +++++++++++++++++++++++++++++++++ 3 files changed, 601 insertions(+), 44 deletions(-) create mode 100644 tests/HTTP/ClientTest.php.orig diff --git a/lib/Client.php b/lib/Client.php index 5940662..2527bcf 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -66,6 +66,8 @@ class Client extends EventEmitter */ protected $maxRedirects = 5; + protected $headerLinesMap = []; + /** * Initializes the client. */ @@ -73,12 +75,19 @@ public function __construct() { $this->curlSettings = [ CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, + CURLOPT_HEADERFUNCTION => [$this, 'receiveCurlHeader'], CURLOPT_NOBODY => false, CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', ]; } + protected function receiveCurlHeader($curlHandle, $headerLine) + { + $this->headerLinesMap[(int) $curlHandle][] = $headerLine; + + return strlen($headerLine); + } + /** * Sends a request to a HTTP server, and returns a response. */ @@ -414,7 +423,7 @@ protected function createCurlSettingsArray(RequestInterface $request): array * * @param resource $curlHandle */ - protected function parseCurlResult(string $response, $curlHandle): array + protected function parseCurlResult(string $body, $curlHandle): array { list( $curlInfo, @@ -430,36 +439,21 @@ protected function parseCurlResult(string $response, $curlHandle): array ]; } - $headerBlob = substr($response, 0, $curlInfo['header_size']); - // In the case of 204 No Content, strlen($response) == $curlInfo['header_size]. - // This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL - // An exception will be thrown when calling getBodyAsString then - $responseBody = substr($response, $curlInfo['header_size']) ?: null; - - unset($response); - - // In the case of 100 Continue, or redirects we'll have multiple lists - // of headers for each separate HTTP response. We can easily split this - // because they are separated by \r\n\r\n - $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n")); - - // We only care about the last set of headers - $headerBlob = $headerBlob[count($headerBlob) - 1]; - - // Splitting headers - $headerBlob = explode("\r\n", $headerBlob); - $response = new Response(); $response->setStatus($curlInfo['http_code']); - foreach ($headerBlob as $header) { - $parts = explode(':', $header, 2); - if (2 === count($parts)) { - $response->addHeader(trim($parts[0]), trim($parts[1])); + $resourceId = (int) $curlHandle; + if (isset($this->headerLinesMap[$resourceId])) { + foreach ($this->headerLinesMap[$resourceId] as $header) { + $parts = explode(':', $header, 2); + if (2 === count($parts)) { + $response->addHeader(trim($parts[0]), trim($parts[1])); + } } + $this->headerLinesMap[$resourceId] = []; } - $response->setBody($responseBody); + $response->setBody($body); $httpCode = $response->getStatus(); @@ -487,7 +481,10 @@ protected function sendAsyncInternal(RequestInterface $request, callable $succes $this->createCurlSettingsArray($request) ); curl_multi_add_handle($this->curlMultiHandle, $curl); - $this->curlMultiMap[(int) $curl] = [ + + $resourceId = (int) $curl; + $this->headerLinesMap[$resourceId] = []; + $this->curlMultiMap[$resourceId] = [ $request, $success, $error, @@ -506,6 +503,8 @@ protected function sendAsyncInternal(RequestInterface $request, callable $succes */ protected function curlExec($curlHandle): string { + $this->headerLinesMap[(int) $curlHandle] = []; + return curl_exec($curlHandle); } diff --git a/tests/HTTP/ClientTest.php b/tests/HTTP/ClientTest.php index 24f2a4b..942aed4 100644 --- a/tests/HTTP/ClientTest.php +++ b/tests/HTTP/ClientTest.php @@ -15,7 +15,7 @@ public function testCreateCurlSettingsArrayGET() $settings = [ CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], CURLOPT_POSTREDIR => 0, CURLOPT_HTTPHEADER => ['X-Foo: bar'], CURLOPT_NOBODY => false, @@ -41,7 +41,7 @@ public function testCreateCurlSettingsArrayHEAD() $settings = [ CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], CURLOPT_NOBODY => true, CURLOPT_CUSTOMREQUEST => 'HEAD', CURLOPT_HTTPHEADER => ['X-Foo: bar'], @@ -75,7 +75,7 @@ public function testCreateCurlSettingsArrayGETAfterHEAD() $settings = [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], CURLOPT_HTTPHEADER => ['X-Foo: bar'], CURLOPT_NOBODY => false, CURLOPT_URL => 'http://example.org/', @@ -102,7 +102,7 @@ public function testCreateCurlSettingsArrayPUTStream() $settings = [ CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], CURLOPT_PUT => true, CURLOPT_INFILE => $h, CURLOPT_NOBODY => false, @@ -129,7 +129,7 @@ public function testCreateCurlSettingsArrayPUTString() $settings = [ CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], CURLOPT_NOBODY => false, CURLOPT_POSTFIELDS => 'boo', CURLOPT_CUSTOMREQUEST => 'PUT', @@ -357,8 +357,9 @@ public function testParseCurlResult() ]; }); - $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; - $result = $client->parseCurlResult($body, 'foobar'); + $client->receiveCurlHeader(null, "HTTP/1.1 200 OK\r\n"); + $client->receiveCurlHeader(null, "Header1: Val1\r\n"); + $result = $client->parseCurlResult('Foo', 'foobar'); $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); $this->assertEquals(200, $result['http_code']); @@ -374,13 +375,11 @@ public function testParseCurlLargeResult() { ini_set('memory_limit', '70M'); - $header = "HTTP/1.1 200 OK\r\nHeader1: Val1\r\n\r\n"; - $client = new ClientMock(); - $client->on('curlStuff', function (&$return) use ($header) { + $client->on('curlStuff', function (&$return) { $return = [ [ - 'header_size' => strlen($header), + 'header_size' => 34, 'http_code' => 200, ], 0, @@ -388,9 +387,11 @@ public function testParseCurlLargeResult() ]; }); + $client->receiveCurlHeader(null, "HTTP/1.1 200 OK\r\n"); + $client->receiveCurlHeader(null, "Header1: Val1\r\n"); + $body = str_repeat('x', 30 * pow(1024, 2)); - $response = $header . $body; - $result = $client->parseCurlResult($response, 'foobar'); + $result = $client->parseCurlResult($body, 'foobar'); $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); $this->assertEquals(200, $result['http_code']); @@ -409,7 +410,7 @@ public function testParseCurlError() ]; }); - $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + $body = 'Foo'; $result = $client->parseCurlResult($body, 'foobar'); $this->assertEquals(Client::STATUS_CURLERROR, $result['status']); @@ -422,12 +423,11 @@ public function testDoRequest() $client = new ClientMock(); $request = new Request('GET', 'http://example.org/'); $client->on('curlExec', function (&$return) { - $return = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + $return = 'Foo'; }); $client->on('curlStuff', function (&$return) { $return = [ [ - 'header_size' => 33, 'http_code' => 200, ], 0, @@ -436,7 +436,6 @@ public function testDoRequest() }); $response = $client->doRequest($request); $this->assertEquals(200, $response->getStatus()); - $this->assertEquals(['Header1' => ['Val1']], $response->getHeaders()); $this->assertEquals('Foo', $response->getBodyAsString()); } @@ -469,6 +468,14 @@ class ClientMock extends Client { protected $persistedSettings = []; + /** + * Making this method public. + */ + public function receiveCurlHeader($curlHandle, $headerLine) + { + return parent::receiveCurlHeader($curlHandle, $headerLine); + } + /** * Making this method public. */ diff --git a/tests/HTTP/ClientTest.php.orig b/tests/HTTP/ClientTest.php.orig new file mode 100644 index 0000000..95ddf03 --- /dev/null +++ b/tests/HTTP/ClientTest.php.orig @@ -0,0 +1,551 @@ +addCurlSetting(CURLOPT_POSTREDIR, 0); + + $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], + CURLOPT_POSTREDIR => 0, + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_NOBODY => false, + CURLOPT_URL => 'http://example.org/', + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayHEAD() + { + $client = new ClientMock(); + $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], + CURLOPT_NOBODY => true, + CURLOPT_CUSTOMREQUEST => 'HEAD', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayGETAfterHEAD() + { + $client = new ClientMock(); + $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']); + + // Parsing the settings for this method, and discarding the result. + // This will cause the client to automatically persist previous + // settings and will help us detect problems. + $client->createCurlSettingsArray($request); + + // This is the real request. + $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_NOBODY => false, + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayPUTStream() + { + $client = new ClientMock(); + + $h = fopen('php://memory', 'r+'); + fwrite($h, 'booh'); + $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], $h); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], + CURLOPT_PUT => true, + CURLOPT_INFILE => $h, + CURLOPT_NOBODY => false, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayPUTString() + { + $client = new ClientMock(); + $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], 'boo'); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADERFUNCTION => [$client, 'receiveCurlHeader'], + CURLOPT_NOBODY => false, + CURLOPT_POSTFIELDS => 'boo', + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testIssue89MultiplePutInfileGivesWarning() + { + $client = new ClientMock(); + $tmpFile = tmpfile(); + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body'); + + $settings = $client->createCurlSettingsArray($request); + $this->assertArrayNotHasKey(CURLOPT_PUT, $settings); + $this->assertArrayNotHasKey(CURLOPT_INFILE, $settings); + + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], $tmpFile); + + $settings = $client->createCurlSettingsArray($request); + $this->assertEquals(true, $settings[CURLOPT_PUT]); + $this->assertEquals($tmpFile, $settings[CURLOPT_INFILE]); + + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body'); + + $settings = $client->createCurlSettingsArray($request); + $this->assertArrayNotHasKey(CURLOPT_PUT, $settings); + $this->assertArrayNotHasKey(CURLOPT_INFILE, $settings); + } + + public function testSend() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(200); + }); + + $response = $client->send($request); + + $this->assertEquals(200, $response->getStatus()); + } + + /** + * @group ci + */ + public function testSendAsync() + { + $url = $this->getAbsoluteUrl('/foo'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $client = new Client(); + + $request = new Request('GET', $url); + $client->sendAsync($request, function(ResponseInterface $response) { + $this->assertEquals("foo\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(4, $response->getHeader('Content-Length')); + }, function($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to GET $url"); + }); + + $client->wait(); + } + + /** + * @group ci + */ + public function testSendAsynConsecutively() + { + $url = $this->getAbsoluteUrl('/foo'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $client = new Client(); + + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("foo\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(4, $response->getHeader('Content-Length')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to get $url"); + }); + + + $url = $this->getAbsoluteUrl('/bar.php'); + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("bar\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('Bar', $response->getHeader('X-Test')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to get $url"); + }); + + $client->wait(); + } + + public function testSendClientError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + throw new ClientException('aaah', 1); + }); + $called = false; + $client->on('exception', function () use (&$called) { + $called = true; + }); + + try { + $client->send($request); + $this->fail('send() should have thrown an exception'); + } catch (ClientException $e) { + } + $this->assertTrue($called); + } + + public function testSendHttpError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(404); + }); + $called = 0; + $client->on('error', function () use (&$called) { + ++$called; + }); + $client->on('error:404', function () use (&$called) { + ++$called; + }); + + $client->send($request); + $this->assertEquals(2, $called); + } + + public function testSendRetry() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $called = 0; + $client->on('doRequest', function ($request, &$response) use (&$called) { + ++$called; + if ($called < 3) { + $response = new Response(404); + } else { + $response = new Response(200); + } + }); + + $errorCalled = 0; + $client->on('error', function ($request, $response, &$retry, $retryCount) use (&$errorCalled) { + ++$errorCalled; + $retry = true; + }); + + $response = $client->send($request); + $this->assertEquals(3, $called); + $this->assertEquals(2, $errorCalled); + $this->assertEquals(200, $response->getStatus()); + } + + public function testHttpErrorException() + { + $client = new ClientMock(); + $client->setThrowExceptions(true); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(404); + }); + + try { + $client->send($request); + $this->fail('An exception should have been thrown'); + } catch (ClientHttpException $e) { + $this->assertEquals(404, $e->getHttpStatus()); + $this->assertInstanceOf('Sabre\HTTP\Response', $e->getResponse()); + } + } + + public function testParseCurlResult() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + + $client->receiveCurlHeader(null, "HTTP/1.1 200 OK\r\n"); + $client->receiveCurlHeader(null, "Header1: Val1\r\n"); + $result = $client->parseCurlResult('Foo', 'foobar'); + + $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); + $this->assertEquals(200, $result['http_code']); + $this->assertEquals(200, $result['response']->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders()); + $this->assertEquals('Foo', $result['response']->getBodyAsString()); + } + + /** + * @runInSeparateProcess + */ + public function testParseCurlLargeResult() + { + ini_set('memory_limit', '70M'); + + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 34, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + + $client->receiveCurlHeader(null, "HTTP/1.1 200 OK\r\n"); + $client->receiveCurlHeader(null, "Header1: Val1\r\n"); + + $body = str_repeat('x', 30 * pow(1024, 2)); + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); + $this->assertEquals(200, $result['http_code']); + $this->assertEquals(200, $result['response']->getStatus()); + $this->assertEquals(31457280, strlen($result['response']->getBodyAsString())); + } + + public function testParseCurlError() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [], + 1, + 'Curl error', + ]; + }); + + $body = 'Foo'; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_CURLERROR, $result['status']); + $this->assertEquals(1, $result['curl_errno']); + $this->assertEquals('Curl error', $result['curl_errmsg']); + } + + public function testDoRequest() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + $client->on('curlExec', function (&$return) { + $return = 'Foo'; + }); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'http_code' => 200, + ], + 0, + '', + ]; + }); + $response = $client->doRequest($request); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('Foo', $response->getBodyAsString()); + } + + public function testDoRequestCurlError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + $client->on('curlExec', function (&$return) { + $return = ''; + }); + $client->on('curlStuff', function (&$return) { + $return = [ + [], + 1, + 'Curl error', + ]; + }); + + try { + $response = $client->doRequest($request); + $this->fail('This should have thrown an exception'); + } catch (ClientException $e) { + $this->assertEquals(1, $e->getCode()); + $this->assertEquals('Curl error', $e->getMessage()); + } + } +} + +class ClientMock extends Client +{ + protected $persistedSettings = []; + + /** + * Making this method public. + */ + public function receiveCurlHeader($curlHandle, $headerLine) + { + return parent::receiveCurlHeader($curlHandle, $headerLine); + } + + /** + * Making this method public. + */ + public function createCurlSettingsArray(RequestInterface $request): array + { + return parent::createCurlSettingsArray($request); + } + + /** + * Making this method public. + */ + public function parseCurlResult(string $response, $curlHandle): array + { + return parent::parseCurlResult($response, $curlHandle); + } + + /** + * This method is responsible for performing a single request. + */ + public function doRequest(RequestInterface $request): ResponseInterface + { + $response = null; + $this->emit('doRequest', [$request, &$response]); + + // If nothing modified $response, we're using the default behavior. + if (is_null($response)) { + return parent::doRequest($request); + } else { + return $response; + } + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlStuff($curlHandle): array + { + $return = null; + $this->emit('curlStuff', [&$return]); + + // If nothing modified $return, we're using the default behavior. + if (is_null($return)) { + return parent::curlStuff($curlHandle); + } else { + return $return; + } + } + + /** + * Calls curl_exec. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlExec($curlHandle): string + { + $return = null; + $this->emit('curlExec', [&$return]); + + // If nothing modified $return, we're using the default behavior. + if (is_null($return)) { + return parent::curlExec($curlHandle); + } else { + return $return; + } + } +}