From 774dc86ab4ba3da3a5fd3c9cc9050cfbd4bfa6ad Mon Sep 17 00:00:00 2001 From: saasfreelancer Date: Tue, 28 Sep 2021 18:04:52 +0500 Subject: [PATCH 01/17] feat: allow for http adapter param - Base client class now accepts Http Adapter object as param. - Add http adapter interface --- lib/recurly/base_client.php | 7 +++++-- lib/recurly/http_adapter_interface.php | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 lib/recurly/http_adapter_interface.php diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 691a2d7d..0c4c06ed 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -23,10 +23,13 @@ abstract class BaseClient * * @param string $api_key The API key to use when making requests */ - public function __construct(string $api_key, LoggerInterface $logger = null) + public function __construct(string $api_key, LoggerInterface $logger = null, HttpAdapterInterface $http_adapter = null) { $this->_api_key = $api_key; - $this->http = new HttpAdapter; + if (is_null($http_adapter)) { + $http_adapter = new HttpAdapter; + } + $this->http = $http_adapter; if (is_null($logger)) { $logger = new \Recurly\Logger('Recurly', LogLevel::WARNING); } diff --git a/lib/recurly/http_adapter_interface.php b/lib/recurly/http_adapter_interface.php new file mode 100644 index 00000000..843e7a5b --- /dev/null +++ b/lib/recurly/http_adapter_interface.php @@ -0,0 +1,11 @@ + Date: Fri, 4 Mar 2022 16:46:08 -0500 Subject: [PATCH 02/17] Revert "Merge remote-tracking branch 'github-saas/syed/v3-v2021-02-25/guzzle' into v3-v2021-02-25:" This reverts commit e20809e27769d6d28e80d5893df5027f9bde9d02, reversing changes made to d62bdf252e01fb041ed4d971293a60687cdd03b9. --- lib/recurly/base_client.php | 8 ++------ lib/recurly/http_adapter.php | 2 +- lib/recurly/http_adapter_interface.php | 11 ----------- 3 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 lib/recurly/http_adapter_interface.php diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 413e9467..39fa7f35 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -31,7 +31,7 @@ abstract class BaseClient * In addition to the options managed by BaseClient, it accepts the following options: * - "region" to define the Data Center connection - defaults to "us"; */ - public function __construct(string $api_key, LoggerInterface $logger = null, array $options = [], HttpAdapterInterface $http_adapter = null) + public function __construct(string $api_key, LoggerInterface $logger = null, array $options = []) { $this->_api_key = $api_key; if (isset($options['region'])) { @@ -41,11 +41,7 @@ public function __construct(string $api_key, LoggerInterface $logger = null, arr $this->baseUrl = BaseClient::API_HOSTS[$options['region']]; } - if (is_null($http_adapter)) { - $http_adapter = new HttpAdapter; - } - $this->http = $http_adapter; - + $this->http = new HttpAdapter; if (is_null($logger)) { $logger = new \Recurly\Logger('Recurly', LogLevel::WARNING); } diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index 429488ac..f4bca544 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -11,7 +11,7 @@ /** * @codeCoverageIgnore */ -class HttpAdapter implements HttpAdapterInterface +class HttpAdapter { private static $_default_options = [ 'ignore_errors' => true diff --git a/lib/recurly/http_adapter_interface.php b/lib/recurly/http_adapter_interface.php deleted file mode 100644 index 843e7a5b..00000000 --- a/lib/recurly/http_adapter_interface.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Fri, 4 Mar 2022 18:16:32 -0500 Subject: [PATCH 03/17] Added a connection error class, to start handling basic connection errors. --- lib/recurly/errors/connection_error.php | 6 ++++++ lib/recurly/http_adapter.php | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 lib/recurly/errors/connection_error.php diff --git a/lib/recurly/errors/connection_error.php b/lib/recurly/errors/connection_error.php new file mode 100644 index 00000000..60538cd7 --- /dev/null +++ b/lib/recurly/errors/connection_error.php @@ -0,0 +1,6 @@ + Date: Mon, 7 Mar 2022 16:16:53 -0500 Subject: [PATCH 04/17] WIP on error handling --- lib/recurly/http_adapter.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index 83a67b33..fa875fcb 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -43,8 +43,10 @@ public function execute($method, $url, $body, $headers): array } $options['header'] = $headers_str; $context = stream_context_create(['http' => $options]); - $result = file_get_contents($url, false, $context); - + //error_clear_last(); + $result = file_get_contents("Q".$url, false, $context); + //$error = error_get_last(); + if (!empty($result)) { foreach($http_response_header as $h) { if(preg_match('/Content-Encoding:.*gzip/i', $h)) { @@ -53,11 +55,16 @@ public function execute($method, $url, $body, $headers): array } } else { // handle connection errors that prevented any valid response + //error_log(print_r($error,1)); + if ($error['type']==E_WARNING) { error_log(__FILE__.": ".$error['message']); throw new \Recurly\Errors\ConnectionError($error['message']); } + + //if (!is_resource($context)) { error_log(__FILE__.": stream not created"); throw new \Recurly\Errors\ConnectionError("stream not created"); } + if (is_resource($context)) { + $meta = stream_get_meta_data($context); + if ($meta['timed_out']) { error_log(__FILE__.": timed out"); throw new \Recurly\Errors\ConnectionError("timed out"); } - $meta = stream_get_meta_data($context); - if ($meta['timed_out']) throw new \Recurly\Errors\ConnectionError("timed out"); - - error_log(print_r($meta,1)); + error_log(print_r($meta,1)); + } } return [$result, $http_response_header]; } From d703f673553129378d4d2718f3da7f79b13af7d9 Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 7 Mar 2022 16:32:44 -0500 Subject: [PATCH 05/17] Modified base_client to accept HttpAdapter in "options" parameter. --- lib/recurly/base_client.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 413e9467..5c268a85 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -31,7 +31,7 @@ abstract class BaseClient * In addition to the options managed by BaseClient, it accepts the following options: * - "region" to define the Data Center connection - defaults to "us"; */ - public function __construct(string $api_key, LoggerInterface $logger = null, array $options = [], HttpAdapterInterface $http_adapter = null) + public function __construct(string $api_key, LoggerInterface $logger = null, array $options = []) { $this->_api_key = $api_key; if (isset($options['region'])) { @@ -41,13 +41,16 @@ public function __construct(string $api_key, LoggerInterface $logger = null, arr $this->baseUrl = BaseClient::API_HOSTS[$options['region']]; } - if (is_null($http_adapter)) { - $http_adapter = new HttpAdapter; + if (!isset($options['http_adapter'])) { + $options['http_adapter'] = new HttpAdapter; } - $this->http = $http_adapter; + if (!is_a($options['http_adapter'],'\Recurly\HttpAdapter') { + throw new TypeError("http_adapter option must be of type HttpAdapter"); + } + $this->http = $options['http_adapter']; if (is_null($logger)) { - $logger = new \Recurly\Logger('Recurly', LogLevel::WARNING); + $logger = new \Recurly\Logger('Recurly', LogLevel::WARNING); } $this->_logger = $logger; From 0341e5b81b79f0e0d084a86822083011bc6cd07b Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 7 Mar 2022 17:58:52 -0500 Subject: [PATCH 06/17] Preliminary commit of untested, crappy curl adapter. --- lib/recurly/http_adapter.php | 16 +----- lib/recurly/http_adapter_curl.php | 69 ++++++++++++++++++++++++++ lib/recurly/http_adapter_interface.php | 11 ++++ 3 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 lib/recurly/http_adapter_curl.php create mode 100644 lib/recurly/http_adapter_interface.php diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index fa875fcb..f7f7f1a5 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -11,7 +11,7 @@ /** * @codeCoverageIgnore */ -class HttpAdapter +class HttpAdapter extends HttpAdapterInterface { private static $_default_options = [ 'ignore_errors' => true @@ -43,9 +43,7 @@ public function execute($method, $url, $body, $headers): array } $options['header'] = $headers_str; $context = stream_context_create(['http' => $options]); - //error_clear_last(); $result = file_get_contents("Q".$url, false, $context); - //$error = error_get_last(); if (!empty($result)) { foreach($http_response_header as $h) { @@ -53,18 +51,6 @@ public function execute($method, $url, $body, $headers): array $result = gzdecode($result); } } - } else { - // handle connection errors that prevented any valid response - //error_log(print_r($error,1)); - if ($error['type']==E_WARNING) { error_log(__FILE__.": ".$error['message']); throw new \Recurly\Errors\ConnectionError($error['message']); } - - //if (!is_resource($context)) { error_log(__FILE__.": stream not created"); throw new \Recurly\Errors\ConnectionError("stream not created"); } - if (is_resource($context)) { - $meta = stream_get_meta_data($context); - if ($meta['timed_out']) { error_log(__FILE__.": timed out"); throw new \Recurly\Errors\ConnectionError("timed out"); } - - error_log(print_r($meta,1)); - } } return [$result, $http_response_header]; } diff --git a/lib/recurly/http_adapter_curl.php b/lib/recurly/http_adapter_curl.php new file mode 100644 index 00000000..d2d8afbe --- /dev/null +++ b/lib/recurly/http_adapter_curl.php @@ -0,0 +1,69 @@ + Date: Mon, 7 Mar 2022 18:38:09 -0500 Subject: [PATCH 07/17] Oops, dumb modification got left in, now removed. --- lib/recurly/http_adapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index f7f7f1a5..0cb6a114 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -43,7 +43,7 @@ public function execute($method, $url, $body, $headers): array } $options['header'] = $headers_str; $context = stream_context_create(['http' => $options]); - $result = file_get_contents("Q".$url, false, $context); + $result = file_get_contents($url, false, $context); if (!empty($result)) { foreach($http_response_header as $h) { From 1eec2cca1272737b8961af20286b22708e7c926a Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 7 Mar 2022 18:52:49 -0500 Subject: [PATCH 08/17] how did this typo get through --- lib/recurly/base_client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 5c268a85..93873fc6 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -44,8 +44,8 @@ public function __construct(string $api_key, LoggerInterface $logger = null, arr if (!isset($options['http_adapter'])) { $options['http_adapter'] = new HttpAdapter; } - if (!is_a($options['http_adapter'],'\Recurly\HttpAdapter') { - throw new TypeError("http_adapter option must be of type HttpAdapter"); + if (!is_a($options['http_adapter'],'\Recurly\HttpAdapter')) { + throw new TypeError("http_adapter option must be of type HttpAdapter"); } $this->http = $options['http_adapter']; From 83de8ca54b0864fcca7b686ae4fd91a27da70660 Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 7 Mar 2022 18:53:44 -0500 Subject: [PATCH 09/17] implements, not extends, dammit --- lib/recurly/http_adapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index 0cb6a114..e253bd91 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -11,7 +11,7 @@ /** * @codeCoverageIgnore */ -class HttpAdapter extends HttpAdapterInterface +class HttpAdapter implements HttpAdapterInterface { private static $_default_options = [ 'ignore_errors' => true From 8b9ab3511ba398582b71ad9f106611899c8ec9ca Mon Sep 17 00:00:00 2001 From: Sinus Date: Thu, 10 Mar 2022 13:34:43 -0500 Subject: [PATCH 10/17] fixes for curl client --- lib/recurly/base_client.php | 4 +- lib/recurly/http_adapter_curl.php | 79 +++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 93873fc6..a795731d 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -44,8 +44,8 @@ public function __construct(string $api_key, LoggerInterface $logger = null, arr if (!isset($options['http_adapter'])) { $options['http_adapter'] = new HttpAdapter; } - if (!is_a($options['http_adapter'],'\Recurly\HttpAdapter')) { - throw new TypeError("http_adapter option must be of type HttpAdapter"); + if (!($options['http_adapter'] instanceof HttpAdapterInterface)) { + throw new \TypeError("http_adapter option must implement HttpAdapterInterface"); } $this->http = $options['http_adapter']; diff --git a/lib/recurly/http_adapter_curl.php b/lib/recurly/http_adapter_curl.php index d2d8afbe..60ea2e6e 100644 --- a/lib/recurly/http_adapter_curl.php +++ b/lib/recurly/http_adapter_curl.php @@ -26,43 +26,74 @@ class HttpAdapterCurl implements HttpAdapterInterface public function execute($method, $url, $body, $headers): array { $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $url); - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); + // borrowed from client for API v2 + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, TRUE); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); + // curl_setopt($curl, CURLOPT_CAINFO, self::$CACertPath); + + // Connection: + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curl, CURLOPT_TIMEOUT, 45); + + // Request: + if ($method == "POST") { + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $body); + } elseif ($method == "PUT") { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($curl, CURLOPT_POSTFIELDS, $body); + } elseif ($method == "HEAD") { + curl_setopt($curl, CURLOPT_NOBODY, true); + } elseif ($method != "GET") { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); + } + $req_headers = ['Content-Length: '.strlen($body)]; + array_walk($headers,function($v,$k) use (&$req_headers) { $req_headers[] = "$k: $v"; }); + curl_setopt($curl, CURLOPT_HTTPHEADER, $req_headers); + + // Response: + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, FALSE); + curl_setopt($curl, CURLOPT_MAXREDIRS, 1); curl_setopt($curl, CURLOPT_HEADER, 0); - $flat_headers = array_map($headers,function($k,$v) { return "$k: $v"; }); - curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge($flat_headers, [ - 'Content-Length: '.strlen($body), // The Content-Length header is required by Recurly API infrastructure - ])); - curl_setopt($curl, CURLOPT_POSTFIELDS, $body); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + $response_header = []; curl_setopt($curl, CURLOPT_HEADERFUNCTION, function($curl,$line) use (&$response_header) { $response_header[]=$line; + return strlen($line); }); - + + // Debugging: + if (defined("RECURLY_CURL_DEBUG")) { + curl_setopt($curl, CURLOPT_VERBOSE, true); + $streamVerboseHandle = fopen('php://stdout', 'w+'); + curl_setopt($curl, CURLOPT_STDERR, $streamVerboseHandle); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 0); + } + $result = curl_exec($curl); $curl_errno = curl_errno($curl); $curl_error = curl_error($curl); + $curl_info = curl_getinfo($curl); curl_close($curl); - - if (!empty($result)) { - foreach($response_header as $h) { - if(preg_match('/Content-Encoding:.*gzip/i', $h)) { - $result = gzdecode($result); - } - } + + $response_header[]='_curl_errno: '.$curl_errno; + $response_header[]='_curl_error: '.$curl_error; + $response_header[]='_curl_info: '.serialize($curl_info); + + if (defined("RECURLY_CURL_DEBUG")) { + print_r($req_headers); + print_r($response_header); + print_r($curl_info); + } + + if ($curl_errno==0) { + // no processing needed; curl ungzips as needed } else { // handle connection errors that prevented any valid response //error_log(print_r($error,1)); - if ($error['type']==E_WARNING) { error_log(__FILE__.": ".$error['message']); throw new \Recurly\Errors\ConnectionError($error['message']); } - - //if (!is_resource($context)) { error_log(__FILE__.": stream not created"); throw new \Recurly\Errors\ConnectionError("stream not created"); } - if (is_resource($context)) { - $meta = stream_get_meta_data($context); - if ($meta['timed_out']) { error_log(__FILE__.": timed out"); throw new \Recurly\Errors\ConnectionError("timed out"); } - - error_log(print_r($meta,1)); - } + throw new \Recurly\Errors\ConnectionError("Curl error: ".$curl_error); } return [$result, $response_header]; } From 9b6b3e779d425c7da1272b6b21078e1a29c7397e Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 14 Mar 2022 16:28:56 -0400 Subject: [PATCH 11/17] tests/mock_client now supports http_adapter replacements in options --- tests/mock_client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mock_client.php b/tests/mock_client.php index e2cfb362..a7c60d7c 100644 --- a/tests/mock_client.php +++ b/tests/mock_client.php @@ -14,7 +14,7 @@ class MockClient extends BaseClient public function __construct($logger, $options = []) { parent::__construct("apikey", $logger, $options); - $this->http = (new Generator())->getMock(HttpAdapter::class); + $this->http = $options['http_adapter'] ?: (new Generator())->getMock(HttpAdapter::class); } protected function apiVersion(): string From 11d8872ff49b6c82e841379dea1930ec683357c7 Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 14 Mar 2022 18:06:24 -0400 Subject: [PATCH 12/17] Fallback to guessing error type from HTTP status code even if "Content-Type: application/json" was received in header. --- lib/recurly/recurly_error.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/recurly/recurly_error.php b/lib/recurly/recurly_error.php index e924e48f..48db934d 100644 --- a/lib/recurly/recurly_error.php +++ b/lib/recurly/recurly_error.php @@ -54,13 +54,17 @@ public static function fromResponse(\Recurly\Response $response): \Recurly\Recur $api_error = \Recurly\Resources\ErrorMayHaveTransaction::fromResponse($response, $error); return new $klass($error->message, $api_error); } - } else { - $error_type = static::errorFromStatus($response->getStatusCode()); - $klass = static::titleize($error_type, '\\Recurly\\Errors\\'); - if (class_exists($klass)) { - return new $klass('An unexpected error has occurred'); - } } + + // "Content-type: application/json" may appear without a body after a HEAD request. + // If the above failed, try guessing from the status code. + $error_type = static::errorFromStatus($response->getStatusCode()); + $klass = static::titleize($error_type, '\\Recurly\\Errors\\'); + if (class_exists($klass)) { + return new $klass('An unexpected error has occurred'); + } + + // No valid error type was found, sorry. $klass = static::_defaultErrorType($response); return new $klass('An unexpected error has occurred'); From c66fd99f352b8b21ba610e21d1c7cb34df601b97 Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 14 Mar 2022 19:09:16 -0400 Subject: [PATCH 13/17] Check response validity always, not just on toResource, so that makeRequest AND pagerCount are validated. --- lib/recurly/base_client.php | 1 + lib/recurly/response.php | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 39fa7f35..d93441a8 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -126,6 +126,7 @@ private function _getResponse(\Recurly\Request $request): \Recurly\Response 'response_headers' => $response->getHeaders() ] ); + $response->assertValid(); // throws \Recurly\RecurlyError if response is bad return $response; } diff --git a/lib/recurly/response.php b/lib/recurly/response.php index baf7a988..d59e3fdc 100644 --- a/lib/recurly/response.php +++ b/lib/recurly/response.php @@ -53,6 +53,16 @@ public function getRequest(): \Recurly\Request return $this->_request; } + /** + * Makes sure the response is valid. Throws RecurlyError otherwise. + */ + public function assertValid(): void + { + if ($this->_status_code < 200 || $this->_status_code >= 300) { + throw \Recurly\RecurlyError::fromResponse($this); + } + } + /** * Converts the Response into a \Recurly\RecurlyResource * @@ -60,16 +70,12 @@ public function getRequest(): \Recurly\Request */ public function toResource(): \Recurly\RecurlyResource { - if ($this->_status_code >= 200 && $this->_status_code < 300) { - if (empty($this->_response)) { - return \Recurly\RecurlyResource::fromEmpty($this); - } elseif (in_array($this->getContentType(), static::BINARY_TYPES)) { - return \Recurly\RecurlyResource::fromBinary($this->_response, $this); - } else { - return \Recurly\RecurlyResource::fromResponse($this); - } + if (empty($this->_response)) { + return \Recurly\RecurlyResource::fromEmpty($this); + } elseif (in_array($this->getContentType(), static::BINARY_TYPES)) { + return \Recurly\RecurlyResource::fromBinary($this->_response, $this); } else { - throw \Recurly\RecurlyError::fromResponse($this); + return \Recurly\RecurlyResource::fromResponse($this); } } From 7caf7ce332132c3c3a5e895798305d32d2c62d50 Mon Sep 17 00:00:00 2001 From: Sinus Date: Mon, 14 Mar 2022 19:24:10 -0400 Subject: [PATCH 14/17] Fixed response test to verify assertion, not toResource erroring out, because toResource no longer throws errors - makeRequest does. --- tests/Response_Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Response_Test.php b/tests/Response_Test.php index 6444190e..1481c68c 100644 --- a/tests/Response_Test.php +++ b/tests/Response_Test.php @@ -175,12 +175,12 @@ public function testToResourceEmpty(): void $this->assertInstanceOf(\Recurly\EmptyResource::class, $result); } - public function testToResourceError(): void + public function testAssertValid(): void { $this->expectException(\Recurly\RecurlyError::class); $response = new Response('', $this->request); $response->setHeaders(['HTTP/1.1 403 Forbidden']); - $result = $response->toResource(); + $result = $response->assertValid(); } public function testGetRawResponse(): void From c4423aabf4b5204542dd31be93edd7940021b253 Mon Sep 17 00:00:00 2001 From: Sinus Date: Thu, 17 Mar 2022 16:24:50 -0400 Subject: [PATCH 15/17] Fixed gzip decoding by delegating it to curl --- lib/recurly/http_adapter_curl.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/recurly/http_adapter_curl.php b/lib/recurly/http_adapter_curl.php index 60ea2e6e..d3bc035c 100644 --- a/lib/recurly/http_adapter_curl.php +++ b/lib/recurly/http_adapter_curl.php @@ -35,6 +35,7 @@ public function execute($method, $url, $body, $headers): array // Connection: curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($curl, CURLOPT_TIMEOUT, 45); + curl_setopt($curl, CURLOPT_ENCODING , "gzip"); // Request: if ($method == "POST") { @@ -82,10 +83,11 @@ public function execute($method, $url, $body, $headers): array $response_header[]='_curl_error: '.$curl_error; $response_header[]='_curl_info: '.serialize($curl_info); - if (defined("RECURLY_CURL_DEBUG")) { - print_r($req_headers); - print_r($response_header); - print_r($curl_info); + if (defined("RECURLY_CURL_DEBUG_2")) { + echo "\nHeaders:\n"; print_r($req_headers); + echo "\nResponse h:\n"; print_r($response_header); + echo "\nCurlinfo:\n"; print_r($curl_info); + echo "\nBody:\n"; print_r($result); } if ($curl_errno==0) { From 454d2c3a288ce6db9d9f1d7166b57c0e0ba7dee6 Mon Sep 17 00:00:00 2001 From: Sinus Date: Thu, 17 Mar 2022 16:42:07 -0400 Subject: [PATCH 16/17] Added a test for the curl adapter. WARNING: uses live API. Safeguarded with env vars, skipped by default. --- tests/AdapterCurl_Test.php | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/AdapterCurl_Test.php diff --git a/tests/AdapterCurl_Test.php b/tests/AdapterCurl_Test.php new file mode 100644 index 00000000..c5ff0300 --- /dev/null +++ b/tests/AdapterCurl_Test.php @@ -0,0 +1,61 @@ +markTestSkipped("Tests performing actual (read-only!) API requests need to be enabled with env var PERFORM_ACTUAL_REQUESTS_WITH_CURL=1. Please consider VALID_RECURLY_API_KEY=11223344..., too."); + } + parent::setUp(); + //$this->logger = new Recurly\Logger('Recurly'); + } + + /** + * Test an invalid API key used in a real HEAD request. Expected result: Errors\Unauthorized exception. + */ + function testHeadRequestUnauthorized(): void + { + $this->client = new Recurly\Client("invalid-api-key", $this->logger, ['http_adapter'=>new Recurly\HttpAdapterCurl()]); + $sites = $this->client->listSites(); + $this->expectException(\Recurly\Errors\Unauthorized::class); + $sites->getCount(); + } + + /** + * Test a valid API key used in a HEAD request. Expected result: numeric. + */ + function testHeadRequestValid(): void + { + if (!getenv("VALID_RECURLY_API_KEY")) { + $this->markTestSkipped("Tests performing actual (read-only!) requests on an actual test site need an env var VALID_RECURLY_API_KEY=11223344... ."); + return; + } + $this->client = new Recurly\Client(getenv("VALID_RECURLY_API_KEY"), $this->logger, ['http_adapter'=>new Recurly\HttpAdapterCurl()]); + $sites = $this->client->listSites(); + $this->assertIsNumeric($sites->getCount()); + } + + /** + * Test a valid API key used in a GET request. Expected result: JSON object converted to a Recurly\Resource. + */ + function testGetRequestValid(): void + { + if (!getenv("VALID_RECURLY_API_KEY")) { + $this->markTestSkipped("Tests performing actual (read-only!) requests on an actual test site need an env var VALID_RECURLY_API_KEY=11223344... ."); + return; + } + $this->client = new Recurly\Client(getenv("VALID_RECURLY_API_KEY"), $this->logger, ['http_adapter'=>new Recurly\HttpAdapterCurl()]); + $sites = $this->client->listSites(); + $site = $sites->getFirst(); + $this->assertIsObject($site); + $this->assertIsString($site->getObject()); + $this->assertEquals("site",$site->getObject()); + } +} From 32bc00fd449de26e5a321915ff092e28fab0e65a Mon Sep 17 00:00:00 2001 From: Sinus Date: Thu, 17 Mar 2022 18:47:32 -0400 Subject: [PATCH 17/17] Reworked error reporting in Curl adapter, no longer throwing a RecurlyError, just a ConnectionError that inherits \Error, bearing full curlinfo inside. --- lib/recurly/errors/connection_error.php | 14 +++++++++++++- lib/recurly/http_adapter_curl.php | 18 +++++++++--------- tests/AdapterCurl_Test.php | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/lib/recurly/errors/connection_error.php b/lib/recurly/errors/connection_error.php index 60538cd7..88e537bc 100644 --- a/lib/recurly/errors/connection_error.php +++ b/lib/recurly/errors/connection_error.php @@ -1,6 +1,18 @@ _data; + } + + public function __construct(string $message, int $code = null, $data = null) + { + parent::__construct($message,$code); + $this->_data = $data; + } } \ No newline at end of file diff --git a/lib/recurly/http_adapter_curl.php b/lib/recurly/http_adapter_curl.php index d3bc035c..92aa8a29 100644 --- a/lib/recurly/http_adapter_curl.php +++ b/lib/recurly/http_adapter_curl.php @@ -13,6 +13,7 @@ */ class HttpAdapterCurl implements HttpAdapterInterface { + /** * Performs HTTP request * @@ -79,9 +80,9 @@ public function execute($method, $url, $body, $headers): array $curl_info = curl_getinfo($curl); curl_close($curl); - $response_header[]='_curl_errno: '.$curl_errno; - $response_header[]='_curl_error: '.$curl_error; - $response_header[]='_curl_info: '.serialize($curl_info); + // Cram errors into curl_info to make it a complete diagnostic box + $curl_info['_errno'] = $curl_errno; + $curl_info['_error'] = $curl_error; if (defined("RECURLY_CURL_DEBUG_2")) { echo "\nHeaders:\n"; print_r($req_headers); @@ -90,13 +91,12 @@ public function execute($method, $url, $body, $headers): array echo "\nBody:\n"; print_r($result); } - if ($curl_errno==0) { - // no processing needed; curl ungzips as needed - } else { - // handle connection errors that prevented any valid response - //error_log(print_r($error,1)); - throw new \Recurly\Errors\ConnectionError("Curl error: ".$curl_error); + if ($curl_errno>0) { + throw new \Recurly\Errors\ConnectionError("Curl error: ".$curl_error, $curl_errno, $curl_info); } + + // Cram curl_info into the response header, for when the request is valid. + $response_header[]='_curl_info: '.serialize($curl_info); return [$result, $response_header]; } } diff --git a/tests/AdapterCurl_Test.php b/tests/AdapterCurl_Test.php index c5ff0300..c08109ce 100644 --- a/tests/AdapterCurl_Test.php +++ b/tests/AdapterCurl_Test.php @@ -1,8 +1,11 @@ logger = new Recurly\Logger('Recurly'); } + function testInvalidHostname(): void + { + $this->client = new Recurly\Client("invalid-api-key", $this->logger, ['http_adapter'=>new HttpAdapterCurl_InvalidHost()]); + $this->expectException(\Recurly\Errors\ConnectionError::class); + $this->expectExceptionCode(CURLE_COULDNT_RESOLVE_HOST); + $this->client->listSites()->getCount(); + } + /** * Test an invalid API key used in a real HEAD request. Expected result: Errors\Unauthorized exception. */ @@ -59,3 +70,12 @@ function testGetRequestValid(): void $this->assertEquals("site",$site->getObject()); } } + +/** + * This mock of HttpAdapterCurl will try to connect to an invalid host. + */ +class HttpAdapterCurl_InvalidHost extends Recurly\HttpAdapterCurl { + function execute($method, $url, $body, $headers): array { + return parent::execute($method,"https://wrong.hostname.qwerty",$body,$headers); + } +} \ No newline at end of file