Skip to content

Commit 514dbdb

Browse files
committed
Added enhanced HttpConnector error reporting and accompanying tests.
Added connection tries getter and setter to HttpConnector and accompanying tests.
1 parent 2f63f16 commit 514dbdb

File tree

6 files changed

+174
-10
lines changed

6 files changed

+174
-10
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ install:
1414
- alias composer=composer\ -n && composer selfupdate
1515
- composer validate
1616
- composer --prefer-source install
17-
- composer --prefer-source require satooshi/php-coveralls
1817

1918
script:
2019
- bin/test --coverage-clover=build/logs/clover.xml
2120

2221
after_success:
22+
- composer --prefer-source require satooshi/php-coveralls
2323
- vendor/bin/coveralls -v

src/Porter/Net/Http/HttpConnector.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66
use ScriptFUSION\Porter\Options\EncapsulatedOptions;
77
use ScriptFUSION\Retry\ErrorHandler\ExponentialBackoffErrorHandler;
88

9+
/**
10+
* Fetches data from an HTTP server via the PHP wrapper.
11+
*
12+
* Enhanced error reporting is achieved by ignoring HTTP error codes in the wrapper, instead throwing
13+
* HttpServerException which includes the body of the response in the error message.
14+
*/
915
class HttpConnector extends CachingConnector
1016
{
17+
const DEFAULT_TRIES = 5;
18+
1119
/** @var HttpOptions */
1220
private $options;
1321

@@ -17,20 +25,35 @@ class HttpConnector extends CachingConnector
1725
/** @var string */
1826
private $baseUrl;
1927

28+
/** @var int */
29+
private $tries = self::DEFAULT_TRIES;
30+
2031
public function __construct(HttpOptions $options = null)
2132
{
2233
parent::__construct();
2334

2435
$this->options = $options ?: new HttpOptions;
2536
}
2637

38+
/**
39+
* {@inheritdoc}
40+
*
41+
* @param string $source Source.
42+
* @param EncapsulatedOptions|null $options Optional. Options.
43+
*
44+
* @return string Response.
45+
*
46+
* @throws \InvalidArgumentException Options is not an instance of HttpOptions.
47+
* @throws HttpConnectionException Failed to connect to source.
48+
* @throws HttpServerException Server sent an error code.
49+
*/
2750
public function fetchFreshData($source, EncapsulatedOptions $options = null)
2851
{
2952
if ($options && !$options instanceof HttpOptions) {
3053
throw new \InvalidArgumentException('Options must be an instance of HttpOptions.');
3154
}
3255

33-
return \ScriptFUSION\Retry\retry(5, function () use ($source, $options) {
56+
return \ScriptFUSION\Retry\retry($this->getTries(), function () use ($source, $options) {
3457
if (false === $response = @file_get_contents(
3558
$this->getOrCreateUrlBuilder()->buildUrl(
3659
$source,
@@ -39,13 +62,21 @@ public function fetchFreshData($source, EncapsulatedOptions $options = null)
3962
),
4063
false,
4164
stream_context_create([
42-
'http' => array_merge(
65+
'http' => ['ignore_errors' => true] + array_merge(
4366
$this->options->extractHttpContextOptions(),
4467
$options ? $options->extractHttpContextOptions() : []
4568
),
4669
])
4770
)) {
48-
throw new HttpConnectionException(error_get_last()['message']);
71+
$error = error_get_last();
72+
throw new HttpConnectionException($error['message'], $error['type']);
73+
}
74+
75+
$code = explode(' ', $http_response_header[0], 3)[1];
76+
if ($code < 200 || $code >= 400) {
77+
throw new HttpServerException(
78+
"HTTP server responded with error: \"$http_response_header[0]\".\n\n$response"
79+
);
4980
}
5081

5182
return $response;
@@ -76,4 +107,28 @@ public function setBaseUrl($baseUrl)
76107

77108
return $this;
78109
}
110+
111+
/**
112+
* Gets the maximum number of fetch attempts
113+
*
114+
* @return int
115+
*/
116+
public function getTries()
117+
{
118+
return $this->tries;
119+
}
120+
121+
/**
122+
* Sets the maximum number of fetch attempts.
123+
*
124+
* @param int $tries Maximum fetch attempts.
125+
*
126+
* @return $this
127+
*/
128+
public function setTries($tries)
129+
{
130+
$this->tries = max(1, $tries|0);
131+
132+
return $this;
133+
}
79134
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
namespace ScriptFUSION\Porter\Net\Http;
3+
4+
/**
5+
* The exception that is thrown when the server responds with an error code.
6+
*/
7+
class HttpServerException extends \RuntimeException
8+
{
9+
// Intentionally empty.
10+
}
Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,120 @@
11
<?php
22
namespace ScriptFUSIONTest\Functional\Porter\Net\Http;
33

4+
use ScriptFUSION\Porter\Connector\Connector;
5+
use ScriptFUSION\Porter\Net\Http\HttpConnectionException;
46
use ScriptFUSION\Porter\Net\Http\HttpConnector;
57
use ScriptFUSION\Porter\Net\Http\HttpOptions;
8+
use ScriptFUSION\Porter\Net\Http\HttpServerException;
9+
use ScriptFUSION\Retry\FailingTooHardException;
610
use Symfony\Component\Process\Process;
711

812
final class HttpConnectorTest extends \PHPUnit_Framework_TestCase
913
{
1014
const HOST = 'localhost:12345';
1115
const URI = '/test?baz=qux';
1216

13-
public function testConnectionToLocalWebserver()
17+
private static $dir;
18+
19+
/** @var HttpConnector */
20+
private $connector;
21+
22+
public static function setUpBeforeClass()
23+
{
24+
self::$dir = __DIR__ . '/servers';
25+
}
26+
27+
protected function setUp()
1428
{
15-
$connector = new HttpConnector((new HttpOptions)->addHeader($header = 'Foo: Bar'));
29+
$this->connector = new HttpConnector;
30+
}
1631

17-
$process = (new Process(sprintf('php -S %s feedback.php', self::HOST)))->setWorkingDirectory(__DIR__);
18-
$process->start();
19-
$response = $connector->fetch('http://' . self::HOST . self::URI);
20-
$process->stop();
32+
public function testConnectionToLocalWebserver()
33+
{
34+
$server = $this->startServer('feedback');
35+
$response = $this->fetch(new HttpConnector((new HttpOptions)->addHeader($header = 'Foo: Bar')));
36+
$this->stopServer($server);
2137

2238
self::assertRegExp('[\AGET \Q' . self::HOST . self::URI . '\E HTTP/\d+\.\d+$]m', $response);
2339
self::assertRegExp("[^$header$]m", $response);
2440
}
41+
42+
public function testConnectionTimeout()
43+
{
44+
try {
45+
$this->fetch($this->connector->setTries(1));
46+
} catch (FailingTooHardException $exception) {
47+
self::assertInstanceOf(HttpConnectionException::class, $exception->getPrevious());
48+
49+
return;
50+
}
51+
52+
self::fail('Expected exception not thrown.');
53+
}
54+
55+
public function testErrorResponse()
56+
{
57+
$server = $this->startServer('404');
58+
try {
59+
$this->fetch();
60+
} catch (FailingTooHardException $exception) {
61+
self::assertInstanceOf(HttpServerException::class, $exception->getPrevious(), $exception->getMessage());
62+
self::assertStringEndsWith('foo', $exception->getPrevious()->getMessage());
63+
64+
return;
65+
} finally {
66+
$this->stopServer($server);
67+
}
68+
69+
self::fail('Expected exception not thrown.');
70+
}
71+
72+
public function testOneTry()
73+
{
74+
$this->setExpectedException(FailingTooHardException::class, '1');
75+
76+
$this->fetch($this->connector->setTries(1));
77+
}
78+
79+
public function testDefaultTries()
80+
{
81+
$this->setExpectedException(FailingTooHardException::class, (string)HttpConnector::DEFAULT_TRIES);
82+
83+
$this->fetch();
84+
}
85+
86+
/**
87+
* @param string $script
88+
*
89+
* @return Process
90+
*/
91+
private function startServer($script)
92+
{
93+
$server = (new Process(sprintf('php -S %s %s.php', self::HOST, $script)))->setWorkingDirectory(self::$dir);
94+
$server->start();
95+
96+
return $server;
97+
}
98+
99+
private function stopServer(Process $server)
100+
{
101+
/*
102+
* Bizarrely, process IDs are one less than they should be on Travis.
103+
* See https://github.com/symfony/symfony/issues/19611
104+
*/
105+
if (array_key_exists('TRAVIS', $_SERVER)) {
106+
posix_kill($server->getPid() + 1, SIGINT);
107+
108+
return;
109+
}
110+
111+
$server->stop();
112+
}
113+
114+
private function fetch(Connector $connector = null)
115+
{
116+
$connector = $connector ?: $this->connector;
117+
118+
return $connector->fetch('http://' . self::HOST . self::URI);
119+
}
25120
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
http_response_code(404);
3+
4+
echo 'foo';
File renamed without changes.

0 commit comments

Comments
 (0)