Skip to content

Commit f04365d

Browse files
authored
Merge pull request #26 from clue-labs/error-messages
Improve error reporting by always including target URI in exceptions
2 parents 99300c6 + eb15d61 commit f04365d

File tree

4 files changed

+123
-53
lines changed

4 files changed

+123
-53
lines changed

src/ProxyConnector.php

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ public function connect($uri)
146146

147147
$connecting = $this->connector->connect($proxyUri);
148148

149-
$deferred = new Deferred(function ($_, $reject) use ($connecting) {
149+
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
150150
$reject(new RuntimeException(
151-
'Connection cancelled while waiting for proxy (ECONNABORTED)',
151+
'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
152152
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
153153
));
154154

@@ -160,10 +160,10 @@ public function connect($uri)
160160
});
161161

162162
$headers = $this->headers;
163-
$connecting->then(function (ConnectionInterface $stream) use ($target, $headers, $deferred) {
163+
$connecting->then(function (ConnectionInterface $stream) use ($target, $headers, $deferred, $uri) {
164164
// keep buffering data until headers are complete
165165
$buffer = '';
166-
$stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn) {
166+
$stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn, $uri) {
167167
$buffer .= $chunk;
168168

169169
$pos = strpos($buffer, "\r\n\r\n");
@@ -176,19 +176,29 @@ public function connect($uri)
176176
try {
177177
$response = Psr7\parse_response(substr($buffer, 0, $pos));
178178
} catch (Exception $e) {
179-
$deferred->reject(new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $e));
179+
$deferred->reject(new RuntimeException(
180+
'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
181+
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
182+
$e
183+
));
180184
$stream->close();
181185
return;
182186
}
183187

184188
if ($response->getStatusCode() === 407) {
185189
// map status code 407 (Proxy Authentication Required) to EACCES
186-
$deferred->reject(new RuntimeException('Proxy denied connection due to invalid authentication ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13));
190+
$deferred->reject(new RuntimeException(
191+
'Connection to ' . $uri . ' failed because proxy denied access with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)',
192+
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
193+
));
187194
$stream->close();
188195
return;
189196
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
190197
// map non-2xx status code to ECONNREFUSED
191-
$deferred->reject(new RuntimeException('Proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111));
198+
$deferred->reject(new RuntimeException(
199+
'Connection to ' . $uri . ' failed because proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)',
200+
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
201+
));
192202
$stream->close();
193203
return;
194204
}
@@ -207,23 +217,33 @@ public function connect($uri)
207217

208218
// stop buffering when 8 KiB have been read
209219
if (isset($buffer[8192])) {
210-
$deferred->reject(new RuntimeException('Proxy must not send more than 8 KiB of headers (EMSGSIZE)', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90));
220+
$deferred->reject(new RuntimeException(
221+
'Connection to ' . $uri . ' failed because proxy response headers exceed maximum of 8 KiB (EMSGSIZE)',
222+
defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90
223+
));
211224
$stream->close();
212225
}
213226
});
214227

215-
$stream->on('error', function (Exception $e) use ($deferred) {
216-
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
228+
$stream->on('error', function (Exception $e) use ($deferred, $uri) {
229+
$deferred->reject(new RuntimeException(
230+
'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
231+
defined('SOCKET_EIO') ? SOCKET_EIO : 5,
232+
$e
233+
));
217234
});
218235

219-
$stream->on('close', function () use ($deferred) {
220-
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
236+
$stream->on('close', function () use ($deferred, $uri) {
237+
$deferred->reject(new RuntimeException(
238+
'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response (ECONNRESET)',
239+
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104
240+
));
221241
});
222242

223243
$stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $headers . "\r\n");
224-
}, function (Exception $e) use ($deferred) {
244+
}, function (Exception $e) use ($deferred, $uri) {
225245
$deferred->reject($e = new RuntimeException(
226-
'Unable to connect to proxy (ECONNREFUSED)',
246+
'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
227247
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
228248
$e
229249
));

tests/AbstractTestCase.php

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,37 +32,19 @@ protected function expectCallableOnceWith($value)
3232
$mock
3333
->expects($this->once())
3434
->method('__invoke')
35-
->with($this->equalTo($value));
35+
->with($value);
3636

3737
return $mock;
3838
}
3939

40-
protected function expectCallableOnceWithExceptionCode($code)
40+
protected function expectCallableOnceWithException($class, $message, $code)
4141
{
42-
$mock = $this->createCallableMock();
43-
$mock
44-
->expects($this->once())
45-
->method('__invoke')
46-
->with($this->logicalAnd(
47-
$this->isInstanceOf('Exception'),
48-
$this->callback(function ($e) use ($code) {
49-
return $e->getCode() === $code;
50-
})
51-
));
52-
53-
return $mock;
54-
}
55-
56-
57-
protected function expectCallableOnceParameter($type)
58-
{
59-
$mock = $this->createCallableMock();
60-
$mock
61-
->expects($this->once())
62-
->method('__invoke')
63-
->with($this->isInstanceOf($type));
64-
65-
return $mock;
42+
return $this->expectCallableOnceWith($this->logicalAnd(
43+
$this->isInstanceOf($class),
44+
$this->callback(function (\Exception $e) use ($message, $code) {
45+
return strpos($e->getMessage(), $message) !== false && $e->getCode() === $code;
46+
})
47+
));
6648
}
6749

6850
/**

tests/FunctionalTest.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ public function testNonListeningSocketRejectsConnection()
3434

3535
$promise = $proxy->connect('google.com:80');
3636

37-
$this->setExpectedException('RuntimeException', 'Unable to connect to proxy', SOCKET_ECONNREFUSED);
37+
$this->setExpectedException(
38+
'RuntimeException',
39+
'Connection to tcp://google.com:80 failed because connection to proxy failed (ECONNREFUSED)',
40+
SOCKET_ECONNREFUSED
41+
);
3842
Block\await($promise, $this->loop, 3.0);
3943
}
4044

@@ -44,7 +48,11 @@ public function testPlainGoogleDoesNotAcceptConnectMethod()
4448

4549
$promise = $proxy->connect('google.com:80');
4650

47-
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
51+
$this->setExpectedException(
52+
'RuntimeException',
53+
'Connection to tcp://google.com:80 failed because proxy refused connection with HTTP error code 405 (Method Not Allowed) (ECONNREFUSED)',
54+
SOCKET_ECONNREFUSED
55+
);
4856
Block\await($promise, $this->loop, 3.0);
4957
}
5058

@@ -59,7 +67,11 @@ public function testSecureGoogleDoesNotAcceptConnectMethod()
5967

6068
$promise = $proxy->connect('google.com:80');
6169

62-
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
70+
$this->setExpectedException(
71+
'RuntimeException',
72+
'Connection to tcp://google.com:80 failed because proxy refused connection with HTTP error code 405 (Method Not Allowed) (ECONNREFUSED)',
73+
SOCKET_ECONNREFUSED
74+
);
6375
Block\await($promise, $this->loop, 3.0);
6476
}
6577

@@ -69,7 +81,11 @@ public function testSecureGoogleDoesNotAcceptPlainStream()
6981

7082
$promise = $proxy->connect('google.com:80');
7183

72-
$this->setExpectedException('RuntimeException', 'Connection to proxy lost', SOCKET_ECONNRESET);
84+
$this->setExpectedException(
85+
'RuntimeException',
86+
'Connection to tcp://google.com:80 failed because connection to proxy was lost while waiting for response (ECONNRESET)',
87+
SOCKET_ECONNRESET
88+
);
7389
Block\await($promise, $this->loop, 3.0);
7490
}
7591

tests/ProxyConnectorTest.php

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,16 +285,24 @@ public function testRejectsUriWithNonTcpScheme()
285285
$promise->then(null, $this->expectCallableOnce());
286286
}
287287

288-
public function testRejectsIfConnectorRejects()
288+
public function testRejectsWithPreviousIfConnectorRejects()
289289
{
290-
$promise = \React\Promise\reject(new \RuntimeException());
290+
$promise = \React\Promise\reject($previous = new \RuntimeException());
291291
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
292292

293293
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
294294

295295
$promise = $proxy->connect('google.com:80');
296296

297-
$promise->then(null, $this->expectCallableOnce());
297+
$promise->then(null, $this->expectCallableOnceWithException(
298+
'RuntimeException',
299+
'Connection to tcp://google.com:80 failed because connection to proxy failed (ECONNREFUSED)',
300+
SOCKET_ECONNREFUSED
301+
));
302+
303+
$promise->then(null, $this->expectCallableOnceWith($this->callback(function (\Exception $e) use ($previous) {
304+
return $e->getPrevious() === $previous;
305+
})));
298306
}
299307

300308
public function testRejectsAndClosesIfStreamWritesNonHttp()
@@ -311,7 +319,11 @@ public function testRejectsAndClosesIfStreamWritesNonHttp()
311319
$stream->expects($this->once())->method('close');
312320
$stream->emit('data', array("invalid\r\n\r\n"));
313321

314-
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
322+
$promise->then(null, $this->expectCallableOnceWithException(
323+
'RuntimeException',
324+
'Connection to tcp://google.com:80 failed because proxy returned invalid response (EBADMSG)',
325+
SOCKET_EBADMSG
326+
));
315327
}
316328

317329
public function testRejectsAndClosesIfStreamWritesTooMuchData()
@@ -326,9 +338,13 @@ public function testRejectsAndClosesIfStreamWritesTooMuchData()
326338
$promise = $proxy->connect('google.com:80');
327339

328340
$stream->expects($this->once())->method('close');
329-
$stream->emit('data', array(str_repeat('*', 100000)));
341+
$stream->emit('data', array(str_repeat('*', 10000)));
330342

331-
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EMSGSIZE));
343+
$promise->then(null, $this->expectCallableOnceWithException(
344+
'RuntimeException',
345+
'Connection to tcp://google.com:80 failed because proxy response headers exceed maximum of 8 KiB (EMSGSIZE)',
346+
SOCKET_EMSGSIZE
347+
));
332348
}
333349

334350
public function testRejectsAndClosesIfStreamReturnsProyAuthenticationRequired()
@@ -345,7 +361,11 @@ public function testRejectsAndClosesIfStreamReturnsProyAuthenticationRequired()
345361
$stream->expects($this->once())->method('close');
346362
$stream->emit('data', array("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"));
347363

348-
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EACCES));
364+
$promise->then(null, $this->expectCallableOnceWithException(
365+
'RuntimeException',
366+
'Connection to tcp://google.com:80 failed because proxy denied access with HTTP error code 407 (Proxy Authentication Required) (EACCES)',
367+
SOCKET_EACCES
368+
));
349369
}
350370

351371
public function testRejectsAndClosesIfStreamReturnsNonSuccess()
@@ -362,7 +382,35 @@ public function testRejectsAndClosesIfStreamReturnsNonSuccess()
362382
$stream->expects($this->once())->method('close');
363383
$stream->emit('data', array("HTTP/1.1 403 Not allowed\r\n\r\n"));
364384

365-
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
385+
$promise->then(null, $this->expectCallableOnceWithException(
386+
'RuntimeException',
387+
'Connection to tcp://google.com:80 failed because proxy refused connection with HTTP error code 403 (Not allowed) (ECONNREFUSED)',
388+
SOCKET_ECONNREFUSED
389+
));
390+
}
391+
392+
public function testRejectsWithPreviousExceptionIfStreamEmitsError()
393+
{
394+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
395+
396+
$promise = \React\Promise\resolve($stream);
397+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
398+
399+
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
400+
401+
$promise = $proxy->connect('google.com:80');
402+
403+
$stream->emit('error', array($previous = new \RuntimeException()));
404+
405+
$promise->then(null, $this->expectCallableOnceWithException(
406+
'RuntimeException',
407+
'Connection to tcp://google.com:80 failed because connection to proxy caused a stream error (EIO)',
408+
SOCKET_EIO
409+
));
410+
411+
$promise->then(null, $this->expectCallableOnceWith($this->callback(function (\Exception $e) use ($previous) {
412+
return $e->getPrevious() === $previous;
413+
})));
366414
}
367415

368416
public function testResolvesIfStreamReturnsSuccess()
@@ -423,7 +471,11 @@ public function testCancelPromiseWhileConnectionIsReadyWillCloseOpenConnectionAn
423471

424472
$promise->cancel();
425473

426-
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNABORTED));
474+
$promise->then(null, $this->expectCallableOnceWithException(
475+
'RuntimeException',
476+
'Connection to tcp://google.com:80 cancelled while waiting for proxy (ECONNABORTED)',
477+
SOCKET_ECONNABORTED
478+
));
427479
}
428480

429481
public function testCancelPromiseDuringConnectionShouldNotCreateGarbageCycles()

0 commit comments

Comments
 (0)