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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy