diff --git a/.travis.yml b/.travis.yml
index 04b1391..f4a3f3b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,15 @@
language: php
php:
- - '5.4'
- '5.5'
- '5.6'
- '7.0'
+ - '7.1'
- hhvm
- nightly
before_script:
- composer install
+
+script:
+ - vendor/bin/phpcs
+ - vendor/bin/phpunit
diff --git a/README.md b/README.md
index e61122e..aeec53d 100644
--- a/README.md
+++ b/README.md
@@ -12,22 +12,14 @@ Via [composer](https://getcomposer.org):
`$ composer require "phpcurl/curlhttp"`
##Usage
-
-It is really that easy.
-
```php
setOptions([
- CURLOPT_FOLLOWLOCATION => false, // Any arbitrary curl options you want
-]);
-
$response = $http->post('http://example.com/?a=b', 'my post data', ['User-Agent: My php crawler']);
-// Supported: get(), post(), head(), post(), put(), delete(), custom methods
+// Supported: get(), post(), head(), post(), put(), delete()
$body = $response->getBody(); // Response body, string
diff --git a/composer.json b/composer.json
index 94372b2..5be7e62 100644
--- a/composer.json
+++ b/composer.json
@@ -24,12 +24,13 @@
}
],
"require": {
- "php": ">=5.3.0",
- "phpcurl/curlwrapper": "^1"
+ "php": ">=5.5",
+ "phpcurl/curlwrapper": "^2.1"
},
"require-dev": {
- "phpunit/phpunit": "4.*",
- "weew/php-http-server": "^1.0.0"
+ "phpunit/phpunit": "^4.0 || ^5.0",
+ "squizlabs/php_codesniffer": "^2.0",
+ "symfony/process": "^2"
},
"autoload": {
"psr-4": {
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..1747b77
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,7 @@
+
+
+ src
+ test
+ vendor/*
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 2b4a063..1c138ae 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,8 +1,6 @@
curl = $curl ?: new Curl();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute($url, array $options)
+ {
+ $options[CURLOPT_RETURNTRANSFER] = true;
+ $options[CURLOPT_HEADER] = true;
+
+ $this->curl->init($url);
+ $this->curl->setOptArray($options);
+ $response = $this->curl->exec();
+ if (false === $response) {
+ throw NoResponse::fromCurl($this->curl);
+ }
+ $info = $this->curl->getInfo();
+ return HttpResponse::fromCurl($response, $info);
+ }
+}
diff --git a/src/ExecutorInterface.php b/src/ExecutorInterface.php
new file mode 100644
index 0000000..d99bbd0
--- /dev/null
+++ b/src/ExecutorInterface.php
@@ -0,0 +1,14 @@
+executor = $executor ?: new Executor();
+ }
/**
* HTTP GET
- * @param string $url Goes to curl_init()
- * @param array $headers Same as CURLOPT_HEADER
+ * @param string $url Goes to curl_init()
+ * @param array $headers Same as CURLOPT_HEADER
* @return HttpResponse
*/
- public function get($url, array $headers = null)
+ public function get($url, array $headers = [])
{
- $opt = array();
- if ($headers) {
- $opt[CURLOPT_HTTPHEADER] = $headers;
- }
- return $this->exec($url, $opt);
+ return $this->executor->execute(
+ $url,
+ [
+ CURLOPT_HTTPHEADER => $headers,
+ ]
+ );
}
/**
* HTTP HEAD (implemented using CURLOPT_NOBODY)
- * @param string $url Goes to curl_init()
- * @param array $headers Same as CURLOPT_HEADER
+ * @param string $url Goes to curl_init()
+ * @param array $headers Same as CURLOPT_HEADER
* @return HttpResponse
*/
- public function head($url, array $headers = null)
+ public function head($url, array $headers = [])
{
- $opt[CURLOPT_NOBODY] = true;
- if ($headers !== null) {
- $opt[CURLOPT_HTTPHEADER] = $headers;
- }
- return $this->exec($url, $opt);
+ return $this->executor->execute(
+ $url,
+ [
+ CURLOPT_NOBODY => true,
+ CURLOPT_HTTPHEADER => $headers,
+ ]
+ );
}
/**
* HTTP POST
- * @param string $url Goes to curl_init()
- * @param string|array $data Same as CURLOPT_POSTFIELDS
- * @param array $headers Same as CURLOPT_HEADER
+ * @param string $url Goes to curl_init()
+ * @param string|array $data Same as CURLOPT_POSTFIELDS
+ * @param array $headers Same as CURLOPT_HEADER
* @return HttpResponse
*/
- public function post($url, $data = null, array $headers = null)
+ public function post($url, $data = '', array $headers = [])
{
- $opt[CURLOPT_POST] = true;
- if ($data !== null) {
- $opt[CURLOPT_POSTFIELDS] = $data;
- }
- if ($headers !== null) {
- $opt[CURLOPT_HTTPHEADER] = $headers;
- }
- return $this->exec($url, $opt);
+ return $this->executor->execute(
+ $url,
+ [
+ CURLOPT_POST => true,
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_POSTFIELDS => $data,
+ ]
+ );
}
/**
* HTTP PUT
- * @param string $url Goes to curl_init()
- * @param string|array $data Same as CURLOPT_POSTFIELDS
- * @param array $headers Same as CURLOPT_HEADER
+ * @param string $url Goes to curl_init()
+ * @param string|array $data Same as CURLOPT_POSTFIELDS
+ * @param array $headers Same as CURLOPT_HEADER
* @return HttpResponse
*/
- public function put($url, $data = null, array $headers = null)
+ public function put($url, $data = '', array $headers = [])
{
- return $this->request('PUT', $url, $data, $headers);
+ return $this->executor->execute(
+ $url,
+ [
+ CURLOPT_CUSTOMREQUEST => 'PUT',
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_POSTFIELDS => $data,
+ ]
+ );
}
/**
* HTTP DELETE
- * @param string $url Goes to curl_init()
- * @param array $headers Same as CURLOPT_HEADER
- * @return HttpResponse
- */
- public function delete($url, array $headers = null)
- {
- return $this->request('DELETE', $url, null, $headers);
- }
-
- /**
- * Custom HTTP method
- * @param string $method Goes to CURLOPT_CUSTOMREQUEST
- * @param string $url Goes to curl_init()
- * @param string|array $data Goes to CURLOPT_POSTFIELDS
- * @param array $headers Goes to CURLOPT_HEADER
- * @return HttpResponse
- */
- public function request($method, $url, $data = null, array $headers = null)
- {
- $opt[CURLOPT_CUSTOMREQUEST] = $method;
- if ($headers !== null) {
- $opt[CURLOPT_HTTPHEADER] = $headers;
- }
- if ($data !== null) {
- $opt[CURLOPT_POSTFIELDS] = $data;
- }
- return $this->exec($url, $opt);
- }
-
- /**
- * Set additional CURL options to pass with each request
- * @param array $userOptions Format is the same as curl_setopt_array().
- * Pass an empty array to clear.
- */
- public function setOptions(array $userOptions)
- {
- $this->userOptions = $userOptions;
- }
-
- /**
- * Init curl with $url, set $options, execute, return response
- * @param string $url Goes to curl_init()
- * @param array $options Goes to curl_setopt()
- * @param Curl $curl
+ * @param string $url Goes to curl_init()
+ * @param array $headers Same as CURLOPT_HEADER
* @return HttpResponse
*/
- public function exec($url, array $options, Curl $curl = null)
+ public function delete($url, array $headers = [])
{
- $options[CURLOPT_RETURNTRANSFER] = true;
- $options[CURLOPT_HEADER] = true;
-
- $curl = $curl ?: new Curl();
- $curl->init($url);
- $curl->setOptArray(array_replace_recursive(
- $this->userOptions,
- $options
- ));
-
- $response = $curl->exec($this->attempts, self::USE_EXCEPTIONS);
-
- $info = $curl->getInfo();
- $code = $info['http_code'];
- $headerSize = $info['header_size'];
- $headers = substr($response, 0, $headerSize);
- $headersArray = preg_split("/\r\n/", $headers, -1, PREG_SPLIT_NO_EMPTY);
- $body = substr($response, $headerSize);
- return new HttpResponse($code, $headersArray, $body);
+ return $this->executor->execute(
+ $url,
+ [
+ CURLOPT_CUSTOMREQUEST => 'DELETE',
+ CURLOPT_HTTPHEADER => $headers,
+ ]
+ );
}
}
diff --git a/src/HttpClientInterface.php b/src/HttpClientInterface.php
new file mode 100644
index 0000000..7f99d1a
--- /dev/null
+++ b/src/HttpClientInterface.php
@@ -0,0 +1,47 @@
+status = $status;
- $this->body = $body;
+ $this->status = (int) $status;
+ $this->body = (string) $body;
$this->headers = $headers;
}
+ /**
+ * @param string $response
+ * @param array $info
+ * @return HttpResponse
+ */
+ public static function fromCurl($response, array $info)
+ {
+ $headerSize = $info['header_size'];
+ $headers = substr($response, 0, $headerSize);
+ return new self(
+ $info['http_code'],
+ preg_split("/\r\n/", $headers, -1, PREG_SPLIT_NO_EMPTY),
+ substr($response, $headerSize)
+ );
+ }
+
/**
* @return int
*/
diff --git a/src/NoResponse.php b/src/NoResponse.php
new file mode 100644
index 0000000..33ee5ce
--- /dev/null
+++ b/src/NoResponse.php
@@ -0,0 +1,12 @@
+error(), $curl->errno());
+ }
+}
diff --git a/src/RetryingExecutor.php b/src/RetryingExecutor.php
new file mode 100644
index 0000000..215f39f
--- /dev/null
+++ b/src/RetryingExecutor.php
@@ -0,0 +1,43 @@
+retries = $retries;
+ $this->executor = $executor;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute($url, array $options)
+ {
+ for ($attempt = 0; $attempt <= $this->retries; $attempt++) {
+ try {
+ return $this->executor->execute($url, $options);
+ } catch (NoResponse $exception) {
+ }
+ }
+ throw new NoResponse("No response after $attempt attempts", 0, $exception);
+ }
+}
diff --git a/test/FunctionalTest.php b/test/FunctionalTest.php
new file mode 100644
index 0000000..5cfa32a
--- /dev/null
+++ b/test/FunctionalTest.php
@@ -0,0 +1,184 @@
+start();
+ sleep(1);
+ }
+
+ public static function tearDownAfterClass()
+ {
+ if (isset(self::$server)) {
+ self::$server->stop();
+ }
+ }
+
+ protected function setUp()
+ {
+ $this->client = new HttpClient();
+ }
+
+ public function testGet()
+ {
+ $response = $this->client->get('http://localhost:8080/yo', ['Foo: Bar']);
+ $this->assertEquals(200, $response->getStatus());
+ $this->assertEquals(
+ [
+ 'uri' => '/yo',
+ 'headers' => [
+ 'Foo' => 'Bar',
+ 'Host' => 'localhost:8080',
+ 'Accept' => '*/*',
+ ],
+ 'method' => 'GET',
+ 'post' => [],
+ 'raw_post' => null,
+ ],
+ $this->getRequestData($response)
+ );
+ }
+
+ public function testHead()
+ {
+ $response = $this->client->head('http://localhost:8080/yo', ['Foo: Bar']);
+ $this->assertEquals(200, $response->getStatus());
+ $this->assertEquals(
+ [
+ 'uri' => '/yo',
+ 'headers' => [
+ 'Foo' => 'Bar',
+ 'Host' => 'localhost:8080',
+ 'Accept' => '*/*',
+ ],
+ 'method' => 'HEAD',
+ 'post' => [],
+ 'raw_post' => null,
+ ],
+ $this->getRequestData($response)
+ );
+ }
+
+ public function testPost()
+ {
+ $response = $this->client->post('http://localhost:8080/yo', 'foo=bar', ['Foo: Bar']);
+ $this->assertEquals(200, $response->getStatus());
+ $this->assertEquals(
+ [
+ 'uri' => '/yo',
+ 'headers' => [
+ 'Foo' => 'Bar',
+ 'Host' => 'localhost:8080',
+ 'Accept' => '*/*',
+ 'Content-Length' => '7',
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ ],
+ 'method' => 'POST',
+ 'post' => [
+ 'foo' => 'bar',
+ ],
+ 'raw_post' => 'foo=bar',
+ ],
+ $this->getRequestData($response)
+ );
+ }
+
+ public function testPut()
+ {
+ $response = $this->client->put('http://localhost:8080/yo', 'boo', ['Foo: Bar']);
+ $this->assertEquals(200, $response->getStatus());
+ $this->assertEquals(
+ [
+ 'uri' => '/yo',
+ 'headers' => [
+ 'Foo' => 'Bar',
+ 'Host' => 'localhost:8080',
+ 'Accept' => '*/*',
+ 'Content-Length' => '3',
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ ],
+ 'method' => 'PUT',
+ 'post' => [],
+ 'raw_post' => 'boo',
+ ],
+ $this->getRequestData($response)
+ );
+ }
+
+ public function testDelete()
+ {
+ $response = $this->client->delete('http://localhost:8080/yo', ['Foo: Bar']);
+ $this->assertEquals(200, $response->getStatus());
+ $this->assertEquals(
+ [
+ 'uri' => '/yo',
+ 'headers' => [
+ 'Foo' => 'Bar',
+ 'Host' => 'localhost:8080',
+ 'Accept' => '*/*',
+ ],
+ 'method' => 'DELETE',
+ 'post' => [],
+ 'raw_post' => null,
+ ],
+ $this->getRequestData($response)
+ );
+ }
+
+ /**
+ * @expectedException \PHPCurl\CurlHttp\NoResponse
+ */
+ public function testException()
+ {
+ $response = $this->client->delete('http://localhost:0', ['Foo: Bar']);
+ $this->assertEquals(200, $response->getStatus());
+ $this->assertEquals(
+ [
+ 'uri' => '/yo',
+ 'headers' => [
+ 'Foo' => 'Bar',
+ 'Host' => 'localhost:8080',
+ 'Accept' => '*/*',
+ ],
+ 'method' => 'DELETE',
+ 'post' => [],
+ 'raw_post' => null,
+ ],
+ $this->getRequestData($response)
+ );
+ }
+
+ /**
+ * @param HttpResponse $response
+ * @return array
+ */
+ private function getRequestData(HttpResponse $response)
+ {
+ $prefix = 'Request-Data: ';
+ foreach ($response->getHeaders() as $header) {
+ if (0 === strpos($header, $prefix)) {
+ return unserialize(substr($header, strlen($prefix)));
+ }
+ }
+ throw new \LogicException();
+ }
+}
diff --git a/test/HttpClientFunctionalTest.php b/test/HttpClientFunctionalTest.php
deleted file mode 100644
index b2da960..0000000
--- a/test/HttpClientFunctionalTest.php
+++ /dev/null
@@ -1,106 +0,0 @@
-server = new HttpServer('localhost', 8080, __DIR__.'/helper/server.php');
- $this->server->disableOutput();
- $this->server->start();
-
- $this->client = new HttpClient();
- }
-
- public function tearDown()
- {
- $this->server->stop();
- }
-
- public function testGet()
- {
- $response = $this->client->get('http://localhost:8080/yo', ['Foo: Bar']);
- $server = $this->getRequestData($response);
- $this->assertEquals('/yo', $server['uri']);
- $this->assertEquals('Bar', $server['headers']['Foo']);
- $this->assertEquals('GET', $server['method']);
- $this->assertEquals(null, $server['raw_post']);
- }
-
- public function testHead()
- {
- $response = $this->client->head('http://localhost:8080/yo', ['Foo: Bar']);
- $server = $this->getRequestData($response);
- $this->assertEquals('/yo', $server['uri']);
- $this->assertEquals('Bar', $server['headers']['Foo']);
- $this->assertEquals('HEAD', $server['method']);
- $this->assertEquals(null, $server['raw_post']);
- }
-
- public function testPost()
- {
- $response = $this->client->post('http://localhost:8080/yo', 'boo', ['Foo: Bar']);
- $server = $this->getRequestData($response);
- $this->assertEquals('/yo', $server['uri']);
- $this->assertEquals('Bar', $server['headers']['Foo']);
- $this->assertEquals('POST', $server['method']);
- $this->assertEquals('boo', $server['raw_post']);
- }
-
- public function testPut()
- {
- $response = $this->client->put('http://localhost:8080/yo', 'boo', ['Foo: Bar']);
- $server = $this->getRequestData($response);
- $this->assertEquals('/yo', $server['uri']);
- $this->assertEquals('Bar', $server['headers']['Foo']);
- $this->assertEquals('PUT', $server['method']);
- $this->assertEquals('boo', $server['raw_post']);
- }
-
- public function testDelete()
- {
- $response = $this->client->delete('http://localhost:8080/yo', ['Foo: Bar']);
- $server = $this->getRequestData($response);
- $this->assertEquals('/yo', $server['uri']);
- $this->assertEquals('Bar', $server['headers']['Foo']);
- $this->assertEquals('DELETE', $server['method']);
- $this->assertEquals(null, $server['raw_post']);
- }
-
- public function testCustom()
- {
- $response = $this->client->request('OPTIONS', 'http://localhost:8080/yo', 'boo', ['Foo: Bar']);
- $server = $this->getRequestData($response);
- $this->assertEquals('/yo', $server['uri']);
- $this->assertEquals('Bar', $server['headers']['Foo']);
- $this->assertEquals('OPTIONS', $server['method']);
- $this->assertEquals('boo', $server['raw_post']);
- }
-
- /**
- * @param HttpResponse $response
- * @return array
- */
- private function getRequestData(HttpResponse $response)
- {
- $prefix = 'Request-Data: ';
- foreach ($response->getHeaders() as $header) {
- if (0 === strpos($header, $prefix)) {
- return unserialize(substr($header, strlen($prefix)));
- }
- }
- }
-
-}
diff --git a/test/HttpClientTest.php b/test/HttpClientTest.php
index cfa6b81..d733648 100644
--- a/test/HttpClientTest.php
+++ b/test/HttpClientTest.php
@@ -1,50 +1,117 @@
executor = $this->getMockForAbstractClass(ExecutorInterface::class);
+ $this->http = new HttpClient($this->executor);
+ }
+
+ public function testGet()
+ {
+ $response = new HttpResponse(200, [], 'ok');
+ $this->executor->expects($this->once())
+ ->method('execute')
+ ->with(
+ 'http://example.com',
+ [
+ CURLOPT_HTTPHEADER => ['Foo: Bar']
+ ]
+ )
+ ->willReturn($response);
+ $this->assertEquals(
+ $response,
+ $this->http->get('http://example.com', ['Foo: Bar'])
+ );
+ }
+
+ public function testHead()
{
- $curl = $this->getMockBuilder('PHPCurl\\CurlWrapper\\Curl')
- ->disableOriginalConstructor()
- ->setMethods(array('init', 'exec', 'setOptArray', 'getInfo', '__destruct'))
- ->getMock();
-
- $curl->expects($this->once())
- ->method('init')
- ->with('http://example.com');
-
- $curl->expects($this->once())
- ->method('exec')
- ->willReturn("Age: 42\r\n\r\nHey");
-
- $curl->expects($this->once())
- ->method('getInfo')
- ->willReturn(array(
- 'http_code' => 200,
- 'header_size' => 11
- ));
-
- $curl->expects($this->once())
- ->method('setOptArray')
- ->with(array(
- CURLOPT_BINARYTRANSFER => true,
- CURLOPT_NOBODY => true,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HEADER => true,
- ));
-
- $client = new HttpClient();
- $client->setOptions(array(
- CURLOPT_BINARYTRANSFER => true,
- ));
-
- $response = $client->exec('http://example.com', array(CURLOPT_NOBODY => true), $curl);
-
- $this->assertEquals(200, $response->getStatus());
- $this->assertEquals(array('Age: 42'), $response->getHeaders());
- $this->assertEquals('Hey', $response->getBody());
+ $response = new HttpResponse(200, [], 'ok');
+ $this->executor->expects($this->once())
+ ->method('execute')
+ ->with(
+ 'http://example.com',
+ [
+ CURLOPT_HTTPHEADER => ['Foo: Bar'],
+ CURLOPT_NOBODY => true,
+ ]
+ )
+ ->willReturn($response);
+ $this->assertEquals(
+ $response,
+ $this->http->head('http://example.com', ['Foo: Bar'])
+ );
}
+ public function testPost()
+ {
+ $response = new HttpResponse(200, [], 'ok');
+ $this->executor->expects($this->once())
+ ->method('execute')
+ ->with(
+ 'http://example.com',
+ [
+ CURLOPT_HTTPHEADER => ['Foo: Bar'],
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => 'foo',
+ ]
+ )
+ ->willReturn($response);
+ $this->assertEquals(
+ $response,
+ $this->http->post('http://example.com', 'foo', ['Foo: Bar'])
+ );
+ }
+
+ public function testPut()
+ {
+ $response = new HttpResponse(200, [], 'ok');
+ $this->executor->expects($this->once())
+ ->method('execute')
+ ->with(
+ 'http://example.com',
+ [
+ CURLOPT_HTTPHEADER => ['Foo: Bar'],
+ CURLOPT_CUSTOMREQUEST => 'PUT',
+ CURLOPT_POSTFIELDS => 'foo',
+ ]
+ )
+ ->willReturn($response);
+ $this->assertEquals(
+ $response,
+ $this->http->put('http://example.com', 'foo', ['Foo: Bar'])
+ );
+ }
+
+ public function testDelete()
+ {
+ $response = new HttpResponse(200, [], 'ok');
+ $this->executor->expects($this->once())
+ ->method('execute')
+ ->with(
+ 'http://example.com',
+ [
+ CURLOPT_HTTPHEADER => ['Foo: Bar'],
+ CURLOPT_CUSTOMREQUEST => 'DELETE',
+ ]
+ )
+ ->willReturn($response);
+ $this->assertEquals(
+ $response,
+ $this->http->delete('http://example.com', ['Foo: Bar'])
+ );
+ }
}
diff --git a/test/RetryingExecutorTest.php b/test/RetryingExecutorTest.php
new file mode 100644
index 0000000..ad2026d
--- /dev/null
+++ b/test/RetryingExecutorTest.php
@@ -0,0 +1,76 @@
+getMockForAbstractClass(ExecutorInterface::class));
+ }
+
+ public function successfulCases()
+ {
+ return [
+ [1, 1],
+ [1, 0],
+ [10, 0],
+ [10, 1],
+ [10, 9],
+ ];
+ }
+
+ public function unsuccessfulCases()
+ {
+ return [
+ [1],
+ [10],
+ ];
+ }
+
+ /**
+ * @dataProvider successfulCases
+ * @param $retries
+ * @param $fails
+ */
+ public function testSuccessfulResponse($retries, $fails)
+ {
+ $url = 'https://example.com';
+ $options = [1 => true];
+ $executor = $this->getMockForAbstractClass(ExecutorInterface::class);
+ for ($i = 0; $i < $fails; $i++) {
+ $executor->expects($this->at($i))
+ ->method('execute')
+ ->with($url, $options)
+ ->willThrowException(new NoResponse());
+ }
+ $response = new HttpResponse(200, [], 'ok');
+ $executor->expects($this->at($fails))
+ ->method('execute')
+ ->with($url, $options)
+ ->willReturn($response);
+ $re = new RetryingExecutor($retries, $executor);
+ $this->assertEquals($response, $re->execute($url, $options));
+ }
+
+ /**
+ * @dataProvider unsuccessfulCases
+ * @param $retries
+ * @expectedException \PHPCurl\CurlHttp\NoResponse
+ * @expectedExceptionMessageRegExp /No response after \d+ attempts/
+ */
+ public function testNoResponse($retries)
+ {
+ $url = 'https://example.com';
+ $options = [1 => true];
+ $executor = $this->getMockForAbstractClass(ExecutorInterface::class);
+ $executor->method('execute')
+ ->with($url, $options)
+ ->willThrowException(new NoResponse());
+ $re = new RetryingExecutor($retries, $executor);
+ $re->execute($url, $options);
+ }
+}