Skip to content

Commit 4db274b

Browse files
committed
Improve error reporting when DNS lookup fails (happy eyeballs)
Also fixes a fatal error with legacy PHP 5 where it would fail with a (catchable) fatal error when either DNS lookup fails. During test runs, this would automatically be turned into an Exception and would successfully reject the promise. Without an appropriate error handler, a soft DNS error (such as when IPv6 is not available) would previously terminate the program with a fatal error.
1 parent dfc4559 commit 4db274b

File tree

3 files changed

+45
-10
lines changed

3 files changed

+45
-10
lines changed

src/HappyEyeBallsConnectionBuilder.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public function connect()
8080
};
8181

8282
$that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA));
83-
$that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function ($ips) use ($that, &$timer) {
83+
$that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that, &$timer) {
8484
// happy path: IPv6 has resolved already, continue with IPv4 addresses
8585
if ($that->resolved[Message::TYPE_AAAA] === true) {
8686
return $ips;
@@ -112,22 +112,24 @@ public function connect()
112112

113113
/**
114114
* @internal
115+
* @param int $type DNS query type
116+
* @param callable $reject
117+
* @return \React\Promise\PromiseInterface<string[],\Exception> Returns a promise
118+
* that resolves list of IP addresses on success or rejects with an \Exception on error.
115119
*/
116120
public function resolve($type, $reject)
117121
{
118122
$that = $this;
119-
return $that->resolver->resolveAll($that->host, $type)->then(null, function () use ($type, $reject, $that) {
123+
return $that->resolver->resolveAll($that->host, $type)->then(null, function (\Exception $e) use ($type, $reject, $that) {
120124
unset($that->resolverPromises[$type]);
121125
$that->resolved[$type] = true;
122126

123-
if ($that->hasBeenResolved() === false) {
124-
return;
125-
}
126-
127-
if ($that->ipsCount === 0) {
127+
if ($that->hasBeenResolved() && $that->ipsCount === 0) {
128128
$that->resolverPromises = null;
129-
$reject(new \RuntimeException('Connection to ' . $that->uri . ' failed during DNS lookup: DNS error'));
129+
$reject(new \RuntimeException('Connection to ' . $that->uri . ' failed during DNS lookup: ' . $e->getMessage()));
130130
}
131+
132+
throw $e;
131133
});
132134
}
133135

tests/HappyEyeBallsConnectionBuilderTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@ public function testConnectWillResolveTwiceViaResolver()
3232
$builder->connect();
3333
}
3434

35+
public function testConnectWillRejectWhenBothDnsLookupsReject()
36+
{
37+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
38+
$loop->expects($this->never())->method('addTimer');
39+
40+
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
41+
$connector->expects($this->never())->method('connect');
42+
43+
$resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock();
44+
$resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive(
45+
array('reactphp.org', Message::TYPE_AAAA),
46+
array('reactphp.org', Message::TYPE_A)
47+
)->willReturn(new Promise(function () {
48+
throw new \RuntimeException('DNS lookup error');
49+
}));
50+
51+
$uri = 'tcp://reactphp.org:80';
52+
$host = 'reactphp.org';
53+
$parts = parse_url($uri);
54+
55+
$builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts);
56+
57+
$promise = $builder->connect();
58+
59+
$exception = null;
60+
$promise->then(null, function ($e) use (&$exception) {
61+
$exception = $e;
62+
});
63+
64+
$this->assertInstanceOf('RuntimeException', $exception);
65+
$this->assertEquals('Connection to tcp://reactphp.org:80 failed during DNS lookup: DNS lookup error', $exception->getMessage());
66+
}
67+
3568
public function testConnectWillStartTimerWhenIpv4ResolvesAndIpv6IsPending()
3669
{
3770
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

tests/HappyEyeBallsConnectorTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public function testIpv6ResolvesFirstSoIsTheFirstToConnect(array $ipv6, array $i
176176
$this->connector->connect('scheme://google.com:80/?hostname=google.com');
177177

178178
$this->loop->addTimer(0.07, function () use ($deferred) {
179-
$deferred->reject();
179+
$deferred->reject(new \RuntimeException());
180180
});
181181

182182
$this->loop->run();
@@ -196,7 +196,7 @@ public function testIpv6DoesntResolvesWhileIpv4DoesFirstSoIpv4Connects(array $ip
196196
$this->connector->connect('scheme://google.com:80/?hostname=google.com');
197197

198198
$this->loop->addTimer(0.07, function () use ($deferred) {
199-
$deferred->reject();
199+
$deferred->reject(new \RuntimeException());
200200
});
201201

202202
$this->loop->run();

0 commit comments

Comments
 (0)