From 4aa52679fd71983a31a17bb65a16244fba9a9285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 2 Sep 2021 13:56:24 +0200 Subject: [PATCH 1/7] Preserve original errno and previous exception when using happy eyeballs --- src/HappyEyeBallsConnectionBuilder.php | 12 +++++-- tests/HappyEyeBallsConnectionBuilderTest.php | 35 ++++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index 6183b177..d4510de3 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -143,7 +143,11 @@ public function resolve($type, $reject) } if ($that->hasBeenResolved() && $that->ipsCount === 0) { - $reject(new \RuntimeException($that->error())); + $reject(new \RuntimeException( + $that->error(), + 0, + $e + )); } // Exception already handled above, so don't throw an unhandled rejection here @@ -201,7 +205,11 @@ public function check($resolve, $reject) if ($that->ipsCount === $that->failureCount) { $that->cleanUp(); - $reject(new \RuntimeException($that->error())); + $reject(new \RuntimeException( + $that->error(), + $e->getCode(), + $e + )); } }); diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 1d7f815e..30d16a88 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -62,7 +62,11 @@ public function testConnectWillRejectWhenBothDnsLookupsReject() }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed during DNS lookup: DNS lookup error', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWhenBothDnsLookupsRejectWithDifferentMessages() @@ -98,7 +102,11 @@ public function testConnectWillRejectWhenBothDnsLookupsRejectWithDifferentMessag }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed during DNS lookup. Last error for IPv6: DNS6 error. Previous error for IPv4: DNS4 error', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillStartDelayTimerWhenIpv4ResolvesAndIpv6IsPending() @@ -468,7 +476,10 @@ public function testConnectWillRejectWhenOnlyTcp6ConnectionRejectsAndCancelNextA $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection refused', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -476,7 +487,11 @@ public function testConnectWillRejectWhenOnlyTcp6ConnectionRejectsAndCancelNextA }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv6: Connection refused. Previous error for IPv4: DNS failed', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverStartNextAttemptTimer() @@ -504,7 +519,10 @@ public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverSt $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection refused', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -512,7 +530,11 @@ public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverSt }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv4: Connection refused. Previous error for IPv6: DNS failed', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemptTimerImmediately() @@ -542,7 +564,10 @@ public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemp $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection refused', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -550,7 +575,11 @@ public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemp }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Connection refused', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnectionsRejectAndCancelNextAttemptTimerImmediately() From 6e610d53233eec9358cf8f44a25837e529c0e885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Sep 2021 08:11:17 +0200 Subject: [PATCH 2/7] Consistently use `ECONNABORTED` for cancelled connection attempts --- src/DnsConnector.php | 5 ++++- src/HappyEyeBallsConnectionBuilder.php | 5 ++++- src/SecureConnector.php | 5 ++++- src/TcpConnector.php | 5 ++++- tests/DnsConnectorTest.php | 17 ++++++++++++++--- tests/HappyEyeBallsConnectionBuilderTest.php | 9 +++++++++ tests/HappyEyeBallsConnectorTest.php | 6 +++++- tests/SecureConnectorTest.php | 6 +++++- tests/TcpConnectorTest.php | 6 +++++- 9 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/DnsConnector.php b/src/DnsConnector.php index b68d7ea6..719ec21f 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -91,7 +91,10 @@ function ($_, $reject) use (&$promise, &$resolved, $uri) { // cancellation should reject connection attempt // reject DNS resolution with custom reason, otherwise rely on connection cancellation below if ($resolved === null) { - $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled during DNS lookup')); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during DNS lookup', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); } // (try to) cancel pending DNS lookup / connection attempt diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index d4510de3..130ddfb2 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -103,7 +103,10 @@ public function connect() return $deferred->promise(); })->then($lookupResolve(Message::TYPE_A)); }, function ($_, $reject) use ($that, &$timer) { - $reject(new \RuntimeException('Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : ''))); + $reject(new \RuntimeException( + 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : ''), + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); $_ = $reject = null; $that->cleanUp(); diff --git a/src/SecureConnector.php b/src/SecureConnector.php index e6e85c4d..a4799f32 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -105,7 +105,10 @@ function ($resolve, $reject) use ($promise) { }, function ($_, $reject) use (&$promise, $uri, &$connected) { if ($connected) { - $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled during TLS handshake')); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during TLS handshake', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); } $promise->cancel(); diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 9e0a8bc6..2b1c3338 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -141,7 +141,10 @@ public function connect($uri) } // @codeCoverageIgnoreEnd - throw new \RuntimeException('Connection to ' . $uri . ' cancelled during TCP/IP handshake'); + throw new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during TCP/IP handshake', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + ); }); } } diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 16e73fd6..b3d85ec8 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -167,7 +167,11 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tcp://example.com:80 cancelled during DNS lookup'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tcp://example.com:80 cancelled during DNS lookup', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } @@ -196,7 +200,10 @@ public function testCancelDuringTcpConnectionCancelsTcpConnectionWithTcpRejectio $first = new Deferred(); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($first->promise()); $pending = new Promise\Promise(function () { }, function () { - throw new \RuntimeException('Connection cancelled'); + throw new \RuntimeException( + 'Connection cancelled', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); }); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($pending); @@ -205,7 +212,11 @@ public function testCancelDuringTcpConnectionCancelsTcpConnectionWithTcpRejectio $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection cancelled'); + $this->setExpectedException( + 'RuntimeException', + 'Connection cancelled', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 30d16a88..163e27b0 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -664,7 +664,10 @@ public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndCancelDelayTimer() @@ -701,7 +704,10 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndC }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6ConnectionAttemptAndPendingIpv4LookupAndCancelAttemptTimer() @@ -744,7 +750,10 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6Connection }); $this->assertInstanceOf('RuntimeException', $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } public function testResolveWillReturnResolvedPromiseWithEmptyListWhenDnsResolverFails() diff --git a/tests/HappyEyeBallsConnectorTest.php b/tests/HappyEyeBallsConnectorTest.php index d44b8625..e4cd6e3c 100644 --- a/tests/HappyEyeBallsConnectorTest.php +++ b/tests/HappyEyeBallsConnectorTest.php @@ -275,7 +275,11 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $that->throwRejection($promise); }); - $this->setExpectedException('RuntimeException', 'Connection to tcp://example.com:80 cancelled during DNS lookup'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tcp://example.com:80 cancelled during DNS lookup', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + ); $this->loop->run(); } diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 1e0f4f87..9b31fd8c 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -187,7 +187,11 @@ public function testCancelDuringStreamEncryptionCancelsEncryptionAndClosesConnec $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tls://example.com:80 cancelled during TLS handshake'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tls://example.com:80 cancelled during TLS handshake', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index ee5b480e..35dde6e2 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -324,7 +324,11 @@ public function cancellingConnectionShouldRejectPromise() $promise = $connector->connect($server->getAddress()); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); Block\await($promise, $loop); } From 8178edcbb82774072d260a3ef20bb3b3da92c0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Sep 2021 10:38:18 +0200 Subject: [PATCH 3/7] Consistently use `EINVAL` for invalid connection attempts --- src/Connector.php | 3 ++- src/DnsConnector.php | 5 +++- src/FdServer.php | 20 ++++++++++++---- src/HappyEyeBallsConnector.php | 5 +++- src/SecureConnector.php | 5 +++- src/SocketServer.php | 5 +++- src/TcpConnector.php | 15 +++++++++--- src/TcpServer.php | 15 +++++++++--- src/UnixConnector.php | 10 ++++++-- src/UnixServer.php | 10 ++++++-- tests/ConnectorTest.php | 35 ++++++++++++++++++++++++---- tests/DnsConnectorTest.php | 6 ++++- tests/FdServerTest.php | 12 ++++++++-- tests/FunctionalTcpServerTest.php | 24 +++++++++++++++---- tests/HappyEyeBallsConnectorTest.php | 8 ++++--- tests/SecureConnectorTest.php | 6 ++++- tests/SocketServerTest.php | 18 +++++++++++--- tests/TcpConnectorTest.php | 22 ++++++++++------- tests/TestCase.php | 15 ++++++++++++ tests/UnixConnectorTest.php | 12 ++++++++-- tests/UnixServerTest.php | 8 +++++-- 21 files changed, 209 insertions(+), 50 deletions(-) diff --git a/src/Connector.php b/src/Connector.php index 02c9561b..69ff5864 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -169,7 +169,8 @@ public function connect($uri) if (!isset($this->connectors[$scheme])) { return \React\Promise\reject(new \RuntimeException( - 'No connector available for URI scheme "' . $scheme . '"' + 'No connector available for URI scheme "' . $scheme . '"', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } diff --git a/src/DnsConnector.php b/src/DnsConnector.php index 719ec21f..297e7e83 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -29,7 +29,10 @@ public function connect($uri) } if (!$parts || !isset($parts['host'])) { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $original . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $original . '" is invalid', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $host = \trim($parts['host'], '[]'); diff --git a/src/FdServer.php b/src/FdServer.php index 4032d043..df26b52a 100644 --- a/src/FdServer.php +++ b/src/FdServer.php @@ -81,7 +81,10 @@ public function __construct($fd, LoopInterface $loop = null) $fd = (int) $m[1]; } if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) { - throw new \InvalidArgumentException('Invalid FD number given'); + throw new \InvalidArgumentException( + 'Invalid FD number given', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $this->loop = $loop ?: Loop::get(); @@ -95,7 +98,10 @@ public function __construct($fd, LoopInterface $loop = null) $errno = isset($m[1]) ? (int) $m[1] : 0; $errstr = isset($m[2]) ? $m[2] : $error['message']; - throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr, + $errno + ); } $meta = \stream_get_meta_data($this->master); @@ -105,7 +111,10 @@ public function __construct($fd, LoopInterface $loop = null) $errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88; $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket'; - throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr, + $errno + ); } // Socket should not have a peer address if this is a listening socket. @@ -116,7 +125,10 @@ public function __construct($fd, LoopInterface $loop = null) $errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106; $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected'; - throw new \RuntimeException('Failed to listen on FD ' . $fd . ': ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr, + $errno + ); } // Assume this is a Unix domain socket (UDS) when its listening address doesn't parse as a valid URL with a port. diff --git a/src/HappyEyeBallsConnector.php b/src/HappyEyeBallsConnector.php index f7ea0ecf..5dad8c8b 100644 --- a/src/HappyEyeBallsConnector.php +++ b/src/HappyEyeBallsConnector.php @@ -41,7 +41,10 @@ public function connect($uri) } if (!$parts || !isset($parts['host'])) { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $original . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $original . '" is invalid', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $host = \trim($parts['host'], '[]'); diff --git a/src/SecureConnector.php b/src/SecureConnector.php index a4799f32..66ad1c66 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -34,7 +34,10 @@ public function connect($uri) $parts = \parse_url($uri); if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" is invalid', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $context = $this->context; diff --git a/src/SocketServer.php b/src/SocketServer.php index fa379732..0ca637e7 100644 --- a/src/SocketServer.php +++ b/src/SocketServer.php @@ -52,7 +52,10 @@ public function __construct($uri, array $context = array(), LoopInterface $loop $server = new FdServer($uri, $loop); } else { if (preg_match('#^(?:\w+://)?\d+$#', $uri)) { - throw new \InvalidArgumentException('Invalid URI given'); + throw new \InvalidArgumentException( + 'Invalid URI given', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']); diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 2b1c3338..11d279d4 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -27,12 +27,18 @@ public function connect($uri) $parts = \parse_url($uri); if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" is invalid', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $ip = \trim($parts['host'], '[]'); if (false === \filter_var($ip, \FILTER_VALIDATE_IP)) { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $ip . '" does not contain a valid host IP')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" does not contain a valid host IP', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } // use context given in constructor @@ -125,7 +131,10 @@ public function connect($uri) // @codeCoverageIgnoreEnd \fclose($stream); - $reject(new \RuntimeException('Connection to ' . $uri . ' failed: ' . $errstr, $errno)); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' failed: ' . $errstr, + $errno + )); } else { $resolve(new Connection($stream, $loop)); } diff --git a/src/TcpServer.php b/src/TcpServer.php index 622d5575..c919a0aa 100644 --- a/src/TcpServer.php +++ b/src/TcpServer.php @@ -154,11 +154,17 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a // ensure URI contains TCP scheme, host and port if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { - throw new \InvalidArgumentException('Invalid URI "' . $uri . '" given'); + throw new \InvalidArgumentException( + 'Invalid URI "' . $uri . '" given', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } if (false === \filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException('Given URI "' . $uri . '" does not contain a valid host IP'); + throw new \InvalidArgumentException( + 'Given URI "' . $uri . '" does not contain a valid host IP', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $this->master = @\stream_socket_server( @@ -169,7 +175,10 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a \stream_context_create(array('socket' => $context + array('backlog' => 511))) ); if (false === $this->master) { - throw new \RuntimeException('Failed to listen on "' . $uri . '": ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on "' . $uri . '": ' . $errstr, + $errno + ); } \stream_set_blocking($this->master, false); diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 4cfb5a37..8a1eaa35 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -28,13 +28,19 @@ public function connect($path) if (\strpos($path, '://') === false) { $path = 'unix://' . $path; } elseif (\substr($path, 0, 7) !== 'unix://') { - return Promise\reject(new \InvalidArgumentException('Given URI "' . $path . '" is invalid')); + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $path . '" is invalid', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + )); } $resource = @\stream_socket_client($path, $errno, $errstr, 1.0); if (!$resource) { - return Promise\reject(new \RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); + return Promise\reject(new \RuntimeException( + 'Unable to connect to unix domain socket "' . $path . '": ' . $errstr, + $errno + )); } $connection = new Connection($resource, $this->loop); diff --git a/src/UnixServer.php b/src/UnixServer.php index 25accbe0..5c150146 100644 --- a/src/UnixServer.php +++ b/src/UnixServer.php @@ -57,7 +57,10 @@ public function __construct($path, LoopInterface $loop = null, array $context = if (\strpos($path, '://') === false) { $path = 'unix://' . $path; } elseif (\substr($path, 0, 7) !== 'unix://') { - throw new \InvalidArgumentException('Given URI "' . $path . '" is invalid'); + throw new \InvalidArgumentException( + 'Given URI "' . $path . '" is invalid', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 + ); } $this->master = @\stream_socket_server( @@ -79,7 +82,10 @@ public function __construct($path, LoopInterface $loop = null, array $context = } } - throw new \RuntimeException('Failed to listen on Unix domain socket "' . $path . '": ' . $errstr, $errno); + throw new \RuntimeException( + 'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr, + $errno + ); } \stream_set_blocking($this->master, 0); diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php index f85d8b1e..b0c72c7d 100644 --- a/tests/ConnectorTest.php +++ b/tests/ConnectorTest.php @@ -169,7 +169,12 @@ public function testConnectorWithUnknownSchemeAlwaysFails() $connector = new Connector(array(), $loop); $promise = $connector->connect('unknown://google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "unknown"', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() @@ -180,7 +185,12 @@ public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() ), $loop); $promise = $connector->connect('google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "tcp"', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledTcpSchemeAlwaysFails() @@ -191,7 +201,12 @@ public function testConnectorWithDisabledTcpSchemeAlwaysFails() ), $loop); $promise = $connector->connect('tcp://google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "tcp"', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledTlsSchemeAlwaysFails() @@ -202,7 +217,12 @@ public function testConnectorWithDisabledTlsSchemeAlwaysFails() ), $loop); $promise = $connector->connect('tls://google.com:443'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "tls"', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorWithDisabledUnixSchemeAlwaysFails() @@ -213,7 +233,12 @@ public function testConnectorWithDisabledUnixSchemeAlwaysFails() ), $loop); $promise = $connector->connect('unix://demo.sock'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException', + 'No connector available for URI scheme "unix"', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectorUsesGivenResolverInstance() diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index b3d85ec8..056946f2 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -78,7 +78,11 @@ public function testRejectsImmediatelyIfUriIsInvalid() $promise = $this->connector->connect('////'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "////" is invalid', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectRejectsIfGivenIpAndTcpConnectorRejectsWithRuntimeException() diff --git a/tests/FdServerTest.php b/tests/FdServerTest.php index b23231e4..e23e0057 100644 --- a/tests/FdServerTest.php +++ b/tests/FdServerTest.php @@ -30,7 +30,11 @@ public function testCtorThrowsForInvalidFd() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid FD number given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new FdServer(-1, $loop); } @@ -39,7 +43,11 @@ public function testCtorThrowsForInvalidUrl() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid FD number given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new FdServer('tcp://127.0.0.1:8080', $loop); } diff --git a/tests/FunctionalTcpServerTest.php b/tests/FunctionalTcpServerTest.php index 3f228a06..2e2e4bca 100644 --- a/tests/FunctionalTcpServerTest.php +++ b/tests/FunctionalTcpServerTest.php @@ -350,7 +350,11 @@ public function testFailsToListenOnInvalidUri() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "tcp://///" given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('///', $loop); } @@ -358,7 +362,11 @@ public function testFailsToListenOnUriWithoutPort() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "tcp://127.0.0.1" given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('127.0.0.1', $loop); } @@ -366,7 +374,11 @@ public function testFailsToListenOnUriWithWrongScheme() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "udp://127.0.0.1:0" given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('udp://127.0.0.1:0', $loop); } @@ -374,7 +386,11 @@ public function testFailsToListenOnUriWIthHostname() { $loop = Factory::create(); - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Given URI "tcp://localhost:8080" does not contain a valid host IP', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new TcpServer('localhost:8080', $loop); } } diff --git a/tests/HappyEyeBallsConnectorTest.php b/tests/HappyEyeBallsConnectorTest.php index e4cd6e3c..b5204af3 100644 --- a/tests/HappyEyeBallsConnectorTest.php +++ b/tests/HappyEyeBallsConnectorTest.php @@ -221,9 +221,11 @@ public function testRejectsImmediatelyIfUriIsInvalid() $promise = $this->connector->connect('////'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); - - $this->loop->run(); + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "////" is invalid', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testRejectsWithTcpConnectorRejectionIfGivenIp() diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 9b31fd8c..f838f2fe 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -65,7 +65,11 @@ public function testConnectionToInvalidSchemeWillReject() $promise = $this->connector->connect('tcp://example.com:80'); - $promise->then(null, $this->expectCallableOnce()); + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "tcp://example.com:80" is invalid', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testConnectWillRejectWithTlsUriWhenUnderlyingConnectorRejects() diff --git a/tests/SocketServerTest.php b/tests/SocketServerTest.php index 7092b8b4..bbab3389 100644 --- a/tests/SocketServerTest.php +++ b/tests/SocketServerTest.php @@ -41,19 +41,31 @@ public function testCreateServerWithZeroPortAssignsRandomPort() public function testConstructorWithInvalidUriThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI "tcp://invalid URI" given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new SocketServer('invalid URI'); } public function testConstructorWithInvalidUriWithPortOnlyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new SocketServer('0'); } public function testConstructorWithInvalidUriWithSchemaAndPortOnlyThrows() { - $this->setExpectedException('InvalidArgumentException'); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid URI given', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); new SocketServer('tcp://0'); } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 35dde6e2..d8ecfd38 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -255,10 +255,13 @@ public function connectionToHostnameShouldFailImmediately() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = new TcpConnector($loop); - $connector->connect('www.google.com:80')->then( - $this->expectCallableNever(), - $this->expectCallableOnce() - ); + $promise = $connector->connect('www.google.com:80'); + + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "tcp://www.google.com:80" does not contain a valid host IP', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } /** @test */ @@ -267,10 +270,13 @@ public function connectionToInvalidPortShouldFailImmediately() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = new TcpConnector($loop); - $connector->connect('255.255.255.255:12345678')->then( - $this->expectCallableNever(), - $this->expectCallableOnce() - ); + $promise = $connector->connect('255.255.255.255:12345678'); + + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "tcp://255.255.255.255:12345678" is invalid', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } /** @test */ diff --git a/tests/TestCase.php b/tests/TestCase.php index cdb8b1bc..6010b827 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -41,6 +41,21 @@ protected function expectCallableOnceWith($value) return $mock; } + protected function expectCallableOnceWithException($type, $message = null, $code = null) + { + return $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf($type), + $this->callback(function (\Exception $e) use ($message) { + return $message === null || $e->getMessage() === $message; + }), + $this->callback(function (\Exception $e) use ($code) { + return $code === null || $e->getCode() === $code; + }) + ) + ); + } + protected function expectCallableNever() { $mock = $this->createCallableMock(); diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index 9f68c2d9..c6c2e8eb 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -33,13 +33,21 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() public function testInvalid() { $promise = $this->connector->connect('google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'RuntimeException' + )); } public function testInvalidScheme() { $promise = $this->connector->connect('tcp://google.com:80'); - $promise->then(null, $this->expectCallableOnce()); + + $promise->then(null, $this->expectCallableOnceWithException( + 'InvalidArgumentException', + 'Given URI "tcp://google.com:80" is invalid', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } public function testValid() diff --git a/tests/UnixServerTest.php b/tests/UnixServerTest.php index 463fab12..a383fe2f 100644 --- a/tests/UnixServerTest.php +++ b/tests/UnixServerTest.php @@ -232,8 +232,12 @@ public function testCtorThrowsForInvalidAddressScheme() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->setExpectedException('InvalidArgumentException'); - $server = new UnixServer('tcp://localhost:0', $loop); + $this->setExpectedException( + 'InvalidArgumentException', + 'Given URI "tcp://localhost:0" is invalid', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); + new UnixServer('tcp://localhost:0', $loop); } public function testCtorThrowsWhenPathIsNotWritable() From bd4fe8ae5a0694b554e1d1577edf28b7505ea3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Sep 2021 10:09:28 +0200 Subject: [PATCH 4/7] Consistently report default errno when `ext-sockets` is not available --- src/StreamEncryption.php | 2 +- src/TimeoutConnector.php | 2 +- tests/FunctionalSecureServerTest.php | 8 ++++---- tests/TimeoutConnectorTest.php | 14 +++++++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 8321b699..41ea9f2c 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -126,7 +126,7 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) // EOF or failed without error => connection closed during handshake $d->reject(new \UnexpectedValueException( 'Connection lost during TLS handshake', - \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 0 + \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 )); } else { // handshake failed with error message diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index 02ccceee..7cd64428 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -41,7 +41,7 @@ private static function handler($uri) if ($e instanceof TimeoutException) { throw new \RuntimeException( 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds', - \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 0 + \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 ); } diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index 58b1cf4d..244fa9d7 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -664,11 +664,11 @@ public function testEmitsErrorIfConnectionIsClosedBeforeHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); - // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshak + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake $this->assertInstanceOf('RuntimeException', $error); $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); - $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode()); $this->assertNull($error->getPrevious()); } @@ -692,11 +692,11 @@ public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); - // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshak + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake $this->assertInstanceOf('RuntimeException', $error); $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); - $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode()); $this->assertNull($error->getPrevious()); } diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 98dedca7..3e23543a 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -34,13 +34,17 @@ public function testRejectsWithTimeoutReasonOnTimeout() $timeout = new TimeoutConnector($connector, 0.01, $loop); - $this->setExpectedException('RuntimeException', 'Connection to google.com:80 timed out after 0.01 seconds'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to google.com:80 timed out after 0.01 seconds', + \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 + ); Block\await($timeout->connect('google.com:80'), $loop); } public function testRejectsWithOriginalReasonWhenConnectorRejects() { - $promise = Promise\reject(new \RuntimeException('Failed')); + $promise = Promise\reject(new \RuntimeException('Failed', 42)); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); @@ -49,7 +53,11 @@ public function testRejectsWithOriginalReasonWhenConnectorRejects() $timeout = new TimeoutConnector($connector, 5.0, $loop); - $this->setExpectedException('RuntimeException', 'Failed'); + $this->setExpectedException( + 'RuntimeException', + 'Failed', + 42 + ); Block\await($timeout->connect('google.com:80'), $loop); } From 52f23bb3e41eac052746c39773f728e213d782f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Sep 2021 14:19:27 +0200 Subject: [PATCH 5/7] Append socket error code constants for failed connections --- src/Connector.php | 2 +- src/DnsConnector.php | 4 +-- src/FdServer.php | 8 ++--- src/HappyEyeBallsConnectionBuilder.php | 2 +- src/HappyEyeBallsConnector.php | 2 +- src/SecureConnector.php | 4 +-- src/SocketServer.php | 36 ++++++++++++++++++- src/StreamEncryption.php | 4 +-- src/TcpConnector.php | 10 +++--- src/TcpServer.php | 6 ++-- src/TimeoutConnector.php | 2 +- src/UnixConnector.php | 4 +-- src/UnixServer.php | 4 +-- tests/ConnectorTest.php | 10 +++--- tests/DnsConnectorTest.php | 4 +-- tests/FdServerTest.php | 12 +++---- tests/FunctionalConnectorTest.php | 2 +- tests/FunctionalSecureServerTest.php | 8 ++--- tests/FunctionalTcpServerTest.php | 8 ++--- tests/HappyEyeBallsConnectionBuilderTest.php | 34 +++++++++++------- tests/HappyEyeBallsConnectorTest.php | 4 +-- tests/SecureConnectorTest.php | 37 +++++++++++++++----- tests/ServerTest.php | 2 +- tests/SocketServerTest.php | 8 ++--- tests/TcpConnectorTest.php | 14 +++++--- tests/TcpServerTest.php | 2 +- tests/TimeoutConnectorTest.php | 2 +- tests/UnixConnectorTest.php | 2 +- tests/UnixServerTest.php | 4 +-- 29 files changed, 155 insertions(+), 86 deletions(-) diff --git a/src/Connector.php b/src/Connector.php index 69ff5864..93477bd7 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -169,7 +169,7 @@ public function connect($uri) if (!isset($this->connectors[$scheme])) { return \React\Promise\reject(new \RuntimeException( - 'No connector available for URI scheme "' . $scheme . '"', + 'No connector available for URI scheme "' . $scheme . '" (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } diff --git a/src/DnsConnector.php b/src/DnsConnector.php index 297e7e83..0b51a52c 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -30,7 +30,7 @@ public function connect($uri) if (!$parts || !isset($parts['host'])) { return Promise\reject(new \InvalidArgumentException( - 'Given URI "' . $original . '" is invalid', + 'Given URI "' . $original . '" is invalid (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } @@ -95,7 +95,7 @@ function ($_, $reject) use (&$promise, &$resolved, $uri) { // reject DNS resolution with custom reason, otherwise rely on connection cancellation below if ($resolved === null) { $reject(new \RuntimeException( - 'Connection to ' . $uri . ' cancelled during DNS lookup', + 'Connection to ' . $uri . ' cancelled during DNS lookup (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 )); } diff --git a/src/FdServer.php b/src/FdServer.php index df26b52a..2c7a6c4d 100644 --- a/src/FdServer.php +++ b/src/FdServer.php @@ -82,7 +82,7 @@ public function __construct($fd, LoopInterface $loop = null) } if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) { throw new \InvalidArgumentException( - 'Invalid FD number given', + 'Invalid FD number given (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 ); } @@ -99,7 +99,7 @@ public function __construct($fd, LoopInterface $loop = null) $errstr = isset($m[2]) ? $m[2] : $error['message']; throw new \RuntimeException( - 'Failed to listen on FD ' . $fd . ': ' . $errstr, + 'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno), $errno ); } @@ -112,7 +112,7 @@ public function __construct($fd, LoopInterface $loop = null) $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket'; throw new \RuntimeException( - 'Failed to listen on FD ' . $fd . ': ' . $errstr, + 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (ENOTSOCK)', $errno ); } @@ -126,7 +126,7 @@ public function __construct($fd, LoopInterface $loop = null) $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected'; throw new \RuntimeException( - 'Failed to listen on FD ' . $fd . ': ' . $errstr, + 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (EISCONN)', $errno ); } diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index 130ddfb2..6bd07168 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -104,7 +104,7 @@ public function connect() })->then($lookupResolve(Message::TYPE_A)); }, function ($_, $reject) use ($that, &$timer) { $reject(new \RuntimeException( - 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : ''), + 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 )); $_ = $reject = null; diff --git a/src/HappyEyeBallsConnector.php b/src/HappyEyeBallsConnector.php index 5dad8c8b..0a7c6ecb 100644 --- a/src/HappyEyeBallsConnector.php +++ b/src/HappyEyeBallsConnector.php @@ -42,7 +42,7 @@ public function connect($uri) if (!$parts || !isset($parts['host'])) { return Promise\reject(new \InvalidArgumentException( - 'Given URI "' . $original . '" is invalid', + 'Given URI "' . $original . '" is invalid (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 66ad1c66..03c6e361 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -35,7 +35,7 @@ public function connect($uri) $parts = \parse_url($uri); if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') { return Promise\reject(new \InvalidArgumentException( - 'Given URI "' . $uri . '" is invalid', + 'Given URI "' . $uri . '" is invalid (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } @@ -109,7 +109,7 @@ function ($resolve, $reject) use ($promise) { function ($_, $reject) use (&$promise, $uri, &$connected) { if ($connected) { $reject(new \RuntimeException( - 'Connection to ' . $uri . ' cancelled during TLS handshake', + 'Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 )); } diff --git a/src/SocketServer.php b/src/SocketServer.php index 0ca637e7..c8db84ab 100644 --- a/src/SocketServer.php +++ b/src/SocketServer.php @@ -53,7 +53,7 @@ public function __construct($uri, array $context = array(), LoopInterface $loop } else { if (preg_match('#^(?:\w+://)?\d+$#', $uri)) { throw new \InvalidArgumentException( - 'Invalid URI given', + 'Invalid URI given (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 ); } @@ -121,6 +121,7 @@ public static function accept($socket) foreach (\get_defined_constants(false) as $name => $value) { if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) { $errno = $value; + $errstr .= ' (' . \substr($name, 7) . ')'; break; } } @@ -135,4 +136,37 @@ public static function accept($socket) return $newSocket; } + + /** + * [Internal] Returns errno constant name for given errno value + * + * The errno value describes the type of error that has been encountered. + * This method tries to look up the given errno value and find a matching + * errno constant name which can be useful to provide more context and more + * descriptive error messages. It goes through the list of known errno + * constants when ext-sockets is available to find the matching errno + * constant name. + * + * Because this method is used to append more context to error messages, the + * constant name will be prefixed with a space and put between parenthesis + * when found. + * + * @param int $errno + * @return string e.g. ` (ECONNREFUSED)` or empty string if no matching const for the given errno could be found + * @internal + * @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/errno with permission + * @codeCoverageIgnore + */ + public static function errconst($errno) + { + if (\function_exists('socket_strerror')) { + foreach (\get_defined_constants(false) as $name => $value) { + if ($value === $errno && \strpos($name, 'SOCKET_E') === 0) { + return ' (' . \substr($name, 7) . ')'; + } + } + } + + return ''; + } } diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 41ea9f2c..4aa7fca0 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -125,13 +125,13 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) if (\feof($socket) || $error === null) { // EOF or failed without error => connection closed during handshake $d->reject(new \UnexpectedValueException( - 'Connection lost during TLS handshake', + 'Connection lost during TLS handshake (ECONNRESET)', \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 )); } else { // handshake failed with error message $d->reject(new \UnexpectedValueException( - 'Unable to complete TLS handshake: ' . $error + $error )); } } else { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 11d279d4..6195c6a7 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -28,7 +28,7 @@ public function connect($uri) $parts = \parse_url($uri); if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { return Promise\reject(new \InvalidArgumentException( - 'Given URI "' . $uri . '" is invalid', + 'Given URI "' . $uri . '" is invalid (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } @@ -36,7 +36,7 @@ public function connect($uri) $ip = \trim($parts['host'], '[]'); if (false === \filter_var($ip, \FILTER_VALIDATE_IP)) { return Promise\reject(new \InvalidArgumentException( - 'Given URI "' . $uri . '" does not contain a valid host IP', + 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } @@ -91,7 +91,7 @@ public function connect($uri) if (false === $stream) { return Promise\reject(new \RuntimeException( - \sprintf("Connection to %s failed: %s", $uri, $errstr), + 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno), $errno )); } @@ -132,7 +132,7 @@ public function connect($uri) \fclose($stream); $reject(new \RuntimeException( - 'Connection to ' . $uri . ' failed: ' . $errstr, + 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno), $errno )); } else { @@ -151,7 +151,7 @@ public function connect($uri) // @codeCoverageIgnoreEnd throw new \RuntimeException( - 'Connection to ' . $uri . ' cancelled during TCP/IP handshake', + 'Connection to ' . $uri . ' cancelled during TCP/IP handshake (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 ); }); diff --git a/src/TcpServer.php b/src/TcpServer.php index c919a0aa..f5ae09a1 100644 --- a/src/TcpServer.php +++ b/src/TcpServer.php @@ -155,14 +155,14 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a // ensure URI contains TCP scheme, host and port if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { throw new \InvalidArgumentException( - 'Invalid URI "' . $uri . '" given', + 'Invalid URI "' . $uri . '" given (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 ); } if (false === \filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { throw new \InvalidArgumentException( - 'Given URI "' . $uri . '" does not contain a valid host IP', + 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 ); } @@ -176,7 +176,7 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a ); if (false === $this->master) { throw new \RuntimeException( - 'Failed to listen on "' . $uri . '": ' . $errstr, + 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno), $errno ); } diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index 7cd64428..332369f8 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -40,7 +40,7 @@ private static function handler($uri) return function (\Exception $e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( - 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds', + 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 ); } diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 8a1eaa35..513fb51b 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -29,7 +29,7 @@ public function connect($path) $path = 'unix://' . $path; } elseif (\substr($path, 0, 7) !== 'unix://') { return Promise\reject(new \InvalidArgumentException( - 'Given URI "' . $path . '" is invalid', + 'Given URI "' . $path . '" is invalid (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 )); } @@ -38,7 +38,7 @@ public function connect($path) if (!$resource) { return Promise\reject(new \RuntimeException( - 'Unable to connect to unix domain socket "' . $path . '": ' . $errstr, + 'Unable to connect to unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno), $errno )); } diff --git a/src/UnixServer.php b/src/UnixServer.php index 5c150146..668e8cb3 100644 --- a/src/UnixServer.php +++ b/src/UnixServer.php @@ -58,7 +58,7 @@ public function __construct($path, LoopInterface $loop = null, array $context = $path = 'unix://' . $path; } elseif (\substr($path, 0, 7) !== 'unix://') { throw new \InvalidArgumentException( - 'Given URI "' . $path . '" is invalid', + 'Given URI "' . $path . '" is invalid (EINVAL)', \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22 ); } @@ -83,7 +83,7 @@ public function __construct($path, LoopInterface $loop = null, array $context = } throw new \RuntimeException( - 'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr, + 'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno), $errno ); } diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php index b0c72c7d..b8ac04c2 100644 --- a/tests/ConnectorTest.php +++ b/tests/ConnectorTest.php @@ -172,7 +172,7 @@ public function testConnectorWithUnknownSchemeAlwaysFails() $promise->then(null, $this->expectCallableOnceWithException( 'RuntimeException', - 'No connector available for URI scheme "unknown"', + 'No connector available for URI scheme "unknown" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -188,7 +188,7 @@ public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() $promise->then(null, $this->expectCallableOnceWithException( 'RuntimeException', - 'No connector available for URI scheme "tcp"', + 'No connector available for URI scheme "tcp" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -204,7 +204,7 @@ public function testConnectorWithDisabledTcpSchemeAlwaysFails() $promise->then(null, $this->expectCallableOnceWithException( 'RuntimeException', - 'No connector available for URI scheme "tcp"', + 'No connector available for URI scheme "tcp" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -220,7 +220,7 @@ public function testConnectorWithDisabledTlsSchemeAlwaysFails() $promise->then(null, $this->expectCallableOnceWithException( 'RuntimeException', - 'No connector available for URI scheme "tls"', + 'No connector available for URI scheme "tls" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -236,7 +236,7 @@ public function testConnectorWithDisabledUnixSchemeAlwaysFails() $promise->then(null, $this->expectCallableOnceWithException( 'RuntimeException', - 'No connector available for URI scheme "unix"', + 'No connector available for URI scheme "unix" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 056946f2..2dbc4020 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -80,7 +80,7 @@ public function testRejectsImmediatelyIfUriIsInvalid() $promise->then(null, $this->expectCallableOnceWithException( 'InvalidArgumentException', - 'Given URI "////" is invalid', + 'Given URI "////" is invalid (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -173,7 +173,7 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $this->setExpectedException( 'RuntimeException', - 'Connection to tcp://example.com:80 cancelled during DNS lookup', + 'Connection to tcp://example.com:80 cancelled during DNS lookup (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 ); $this->throwRejection($promise); diff --git a/tests/FdServerTest.php b/tests/FdServerTest.php index e23e0057..3df1b296 100644 --- a/tests/FdServerTest.php +++ b/tests/FdServerTest.php @@ -32,7 +32,7 @@ public function testCtorThrowsForInvalidFd() $this->setExpectedException( 'InvalidArgumentException', - 'Invalid FD number given', + 'Invalid FD number given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new FdServer(-1, $loop); @@ -45,7 +45,7 @@ public function testCtorThrowsForInvalidUrl() $this->setExpectedException( 'InvalidArgumentException', - 'Invalid FD number given', + 'Invalid FD number given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new FdServer('tcp://127.0.0.1:8080', $loop); @@ -64,7 +64,7 @@ public function testCtorThrowsForUnknownFd() $this->setExpectedException( 'RuntimeException', - 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EBADF) : 'Bad file descriptor'), + 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EBADF) . ' (EBADF)' : 'Bad file descriptor'), defined('SOCKET_EBADF') ? SOCKET_EBADF : 9 ); new FdServer($fd, $loop); @@ -85,7 +85,7 @@ public function testCtorThrowsIfFdIsAFileAndNotASocket() $this->setExpectedException( 'RuntimeException', - 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_ENOTSOCK) : 'Not a socket'), + 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_ENOTSOCK) : 'Not a socket') . ' (ENOTSOCK)', defined('SOCKET_ENOTSOCK') ? SOCKET_ENOTSOCK : 88 ); new FdServer($fd, $loop); @@ -108,7 +108,7 @@ public function testCtorThrowsIfFdIsAConnectedSocketInsteadOfServerSocket() $this->setExpectedException( 'RuntimeException', - 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EISCONN) : 'Socket is connected'), + 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EISCONN) : 'Socket is connected') . ' (EISCONN)', defined('SOCKET_EISCONN') ? SOCKET_EISCONN : 106 ); new FdServer($fd, $loop); @@ -361,7 +361,7 @@ public function testEmitsErrorWhenAcceptListenerFails() */ public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception) { - $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT), $exception->getMessage()); + $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode()); } diff --git a/tests/FunctionalConnectorTest.php b/tests/FunctionalConnectorTest.php index f591295a..1536a2ea 100644 --- a/tests/FunctionalConnectorTest.php +++ b/tests/FunctionalConnectorTest.php @@ -168,7 +168,7 @@ public function testCancelPendingTlsConnectionDuringTlsHandshakeShouldCloseTcpCo $this->fail(); } catch (\Exception $e) { $this->assertInstanceOf('RuntimeException', $e); - $this->assertEquals('Connection to ' . $uri . ' cancelled during TLS handshake', $e->getMessage()); + $this->assertEquals('Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)', $e->getMessage()); } } diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index 244fa9d7..b81be4d5 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -664,10 +664,10 @@ public function testEmitsErrorIfConnectionIsClosedBeforeHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); - // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET) $this->assertInstanceOf('RuntimeException', $error); $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); - $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); + $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode()); $this->assertNull($error->getPrevious()); } @@ -692,10 +692,10 @@ public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); - // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET) $this->assertInstanceOf('RuntimeException', $error); $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); - $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); + $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode()); $this->assertNull($error->getPrevious()); } diff --git a/tests/FunctionalTcpServerTest.php b/tests/FunctionalTcpServerTest.php index 2e2e4bca..eae1ceaa 100644 --- a/tests/FunctionalTcpServerTest.php +++ b/tests/FunctionalTcpServerTest.php @@ -352,7 +352,7 @@ public function testFailsToListenOnInvalidUri() $this->setExpectedException( 'InvalidArgumentException', - 'Invalid URI "tcp://///" given', + 'Invalid URI "tcp://///" given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new TcpServer('///', $loop); @@ -364,7 +364,7 @@ public function testFailsToListenOnUriWithoutPort() $this->setExpectedException( 'InvalidArgumentException', - 'Invalid URI "tcp://127.0.0.1" given', + 'Invalid URI "tcp://127.0.0.1" given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new TcpServer('127.0.0.1', $loop); @@ -376,7 +376,7 @@ public function testFailsToListenOnUriWithWrongScheme() $this->setExpectedException( 'InvalidArgumentException', - 'Invalid URI "udp://127.0.0.1:0" given', + 'Invalid URI "udp://127.0.0.1:0" given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new TcpServer('udp://127.0.0.1:0', $loop); @@ -388,7 +388,7 @@ public function testFailsToListenOnUriWIthHostname() $this->setExpectedException( 'InvalidArgumentException', - 'Given URI "tcp://localhost:8080" does not contain a valid host IP', + 'Given URI "tcp://localhost:8080" does not contain a valid host IP (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new TcpServer('localhost:8080', $loop); diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 163e27b0..59b1c1fd 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -477,7 +477,7 @@ public function testConnectWillRejectWhenOnlyTcp6ConnectionRejectsAndCancelNextA $promise = $builder->connect(); $deferred->reject(new \RuntimeException( - 'Connection refused', + 'Connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 )); @@ -489,7 +489,7 @@ public function testConnectWillRejectWhenOnlyTcp6ConnectionRejectsAndCancelNextA $this->assertInstanceOf('RuntimeException', $exception); assert($exception instanceof \RuntimeException); - $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv6: Connection refused. Previous error for IPv4: DNS failed', $exception->getMessage()); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv6: Connection refused (ECONNREFUSED). Previous error for IPv4: DNS failed', $exception->getMessage()); $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } @@ -520,7 +520,7 @@ public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverSt $promise = $builder->connect(); $deferred->reject(new \RuntimeException( - 'Connection refused', + 'Connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 )); @@ -532,7 +532,7 @@ public function testConnectWillRejectWhenOnlyTcp4ConnectionRejectsAndWillNeverSt $this->assertInstanceOf('RuntimeException', $exception); assert($exception instanceof \RuntimeException); - $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv4: Connection refused. Previous error for IPv6: DNS failed', $exception->getMessage()); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Last error for IPv4: Connection refused (ECONNREFUSED). Previous error for IPv6: DNS failed', $exception->getMessage()); $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } @@ -565,7 +565,7 @@ public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemp $promise = $builder->connect(); $deferred->reject(new \RuntimeException( - 'Connection refused', + 'Connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 )); @@ -577,7 +577,7 @@ public function testConnectWillRejectWhenAllConnectionsRejectAndCancelNextAttemp $this->assertInstanceOf('RuntimeException', $exception); assert($exception instanceof \RuntimeException); - $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Connection refused', $exception->getMessage()); + $this->assertEquals('Connection to tcp://reactphp.org:80 failed: Connection refused (ECONNREFUSED)', $exception->getMessage()); $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } @@ -593,7 +593,10 @@ public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnection $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->exactly(2))->method('connect')->willReturnOnConsecutiveCalls( $deferred->promise(), - \React\Promise\reject(new \RuntimeException('Connection to tcp://127.0.0.1:80?hostname=localhost failed: Connection refused')) + \React\Promise\reject(new \RuntimeException( + 'Connection to tcp://127.0.0.1:80?hostname=localhost failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )) ); $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); @@ -612,7 +615,10 @@ public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnection $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); $promise = $builder->connect(); - $deferred->reject(new \RuntimeException('Connection to tcp://[::1]:80?hostname=localhost failed: Connection refused')); + $deferred->reject(new \RuntimeException( + 'Connection to tcp://[::1]:80?hostname=localhost failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + )); $exception = null; $promise->then(null, function ($e) use (&$exception) { @@ -620,7 +626,11 @@ public function testConnectWillRejectWithMessageWithoutHostnameWhenAllConnection }); $this->assertInstanceOf('RuntimeException', $exception); - $this->assertEquals('Connection to tcp://localhost:80 failed: Last error for IPv4: Connection to tcp://127.0.0.1:80 failed: Connection refused. Previous error for IPv6: Connection to tcp://[::1]:80 failed: Connection refused', $exception->getMessage()); + assert($exception instanceof \RuntimeException); + + $this->assertEquals('Connection to tcp://localhost:80 failed: Last error for IPv4: Connection to tcp://127.0.0.1:80 failed: Connection refused (ECONNREFUSED). Previous error for IPv6: Connection to tcp://[::1]:80 failed: Connection refused (ECONNREFUSED)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); + $this->assertInstanceOf('RuntimeException', $exception->getPrevious()); } public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() @@ -666,7 +676,7 @@ public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() $this->assertInstanceOf('RuntimeException', $exception); assert($exception instanceof \RuntimeException); - $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup (ECONNABORTED)', $exception->getMessage()); $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } @@ -706,7 +716,7 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndC $this->assertInstanceOf('RuntimeException', $exception); assert($exception instanceof \RuntimeException); - $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup (ECONNABORTED)', $exception->getMessage()); $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } @@ -752,7 +762,7 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6Connection $this->assertInstanceOf('RuntimeException', $exception); assert($exception instanceof \RuntimeException); - $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled', $exception->getMessage()); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled (ECONNABORTED)', $exception->getMessage()); $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $exception->getCode()); } diff --git a/tests/HappyEyeBallsConnectorTest.php b/tests/HappyEyeBallsConnectorTest.php index b5204af3..6a26fd63 100644 --- a/tests/HappyEyeBallsConnectorTest.php +++ b/tests/HappyEyeBallsConnectorTest.php @@ -223,7 +223,7 @@ public function testRejectsImmediatelyIfUriIsInvalid() $promise->then(null, $this->expectCallableOnceWithException( 'InvalidArgumentException', - 'Given URI "////" is invalid', + 'Given URI "////" is invalid (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -279,7 +279,7 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $this->setExpectedException( 'RuntimeException', - 'Connection to tcp://example.com:80 cancelled during DNS lookup', + 'Connection to tcp://example.com:80 cancelled during DNS lookup (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 ); $this->loop->run(); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index f838f2fe..af3a6f58 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -67,30 +67,44 @@ public function testConnectionToInvalidSchemeWillReject() $promise->then(null, $this->expectCallableOnceWithException( 'InvalidArgumentException', - 'Given URI "tcp://example.com:80" is invalid', + 'Given URI "tcp://example.com:80" is invalid (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } public function testConnectWillRejectWithTlsUriWhenUnderlyingConnectorRejects() { - $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \RuntimeException('Connection to tcp://example.com:80 failed: Connection refused', 42))); + $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \RuntimeException( + 'Connection to tcp://example.com:80 failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ))); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tls://example.com:80 failed: Connection refused', 42); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tls://example.com:80 failed: Connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); $this->throwRejection($promise); } public function testConnectWillRejectWithOriginalMessageWhenUnderlyingConnectorRejectsWithInvalidArgumentException() { - $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \InvalidArgumentException('Invalid', 42))); + $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn(\React\Promise\reject(new \InvalidArgumentException( + 'Invalid', + 42 + ))); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('InvalidArgumentException', 'Invalid', 42); + $this->setExpectedException( + 'InvalidArgumentException', + 'Invalid', + 42 + ); $this->throwRejection($promise); } @@ -105,13 +119,20 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() public function testCancelDuringTcpConnectionCancelsTcpConnectionAndRejectsWithTcpRejection() { - $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException('Connection to tcp://example.com:80 cancelled', 42); }); + $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException( + 'Connection to tcp://example.com:80 cancelled (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); }); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $this->setExpectedException('RuntimeException', 'Connection to tls://example.com:80 cancelled', 42); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tls://example.com:80 cancelled (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + ); $this->throwRejection($promise); } @@ -193,7 +214,7 @@ public function testCancelDuringStreamEncryptionCancelsEncryptionAndClosesConnec $this->setExpectedException( 'RuntimeException', - 'Connection to tls://example.com:80 cancelled during TLS handshake', + 'Connection to tls://example.com:80 cancelled during TLS handshake (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 ); $this->throwRejection($promise); diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 02e10301..b46949ba 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -102,7 +102,7 @@ public function testConstructorThrowsForExistingUnixPath() $this->assertStringEndsWith('Unknown error', $e->getMessage()); } else { $this->assertEquals(SOCKET_EADDRINUSE, $e->getCode()); - $this->assertStringEndsWith('Address already in use', $e->getMessage()); + $this->assertStringEndsWith('Address already in use (EADDRINUSE)', $e->getMessage()); } } } diff --git a/tests/SocketServerTest.php b/tests/SocketServerTest.php index bbab3389..8f453dd1 100644 --- a/tests/SocketServerTest.php +++ b/tests/SocketServerTest.php @@ -43,7 +43,7 @@ public function testConstructorWithInvalidUriThrows() { $this->setExpectedException( 'InvalidArgumentException', - 'Invalid URI "tcp://invalid URI" given', + 'Invalid URI "tcp://invalid URI" given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new SocketServer('invalid URI'); @@ -53,7 +53,7 @@ public function testConstructorWithInvalidUriWithPortOnlyThrows() { $this->setExpectedException( 'InvalidArgumentException', - 'Invalid URI given', + 'Invalid URI given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new SocketServer('0'); @@ -63,7 +63,7 @@ public function testConstructorWithInvalidUriWithSchemaAndPortOnlyThrows() { $this->setExpectedException( 'InvalidArgumentException', - 'Invalid URI given', + 'Invalid URI given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new SocketServer('tcp://0'); @@ -125,7 +125,7 @@ public function testConstructorThrowsForExistingUnixPath() $this->assertStringEndsWith('Unknown error', $e->getMessage()); } else { $this->assertEquals(SOCKET_EADDRINUSE, $e->getCode()); - $this->assertStringEndsWith('Address already in use', $e->getMessage()); + $this->assertStringEndsWith('Address already in use (EADDRINUSE)', $e->getMessage()); } } } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index d8ecfd38..68dc3d76 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -32,7 +32,11 @@ public function connectionToEmptyPortShouldFail() $connector = new TcpConnector($loop); $promise = $connector->connect('127.0.0.1:9999'); - $this->setExpectedException('RuntimeException', 'Connection to tcp://127.0.0.1:9999 failed: Connection refused'); + $this->setExpectedException( + 'RuntimeException', + 'Connection to tcp://127.0.0.1:9999 failed: Connection refused' . (function_exists('socket_import_stream') ? ' (ECONNREFUSED)' : ''), + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); Block\await($promise, $loop, self::TIMEOUT); } @@ -127,7 +131,7 @@ public function connectionToInvalidNetworkShouldFailWithUnreachableError() $this->setExpectedException( 'RuntimeException', - 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) : 'Network is unreachable'), + 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) . ' (ENETUNREACH)' : 'Network is unreachable'), $enetunreach ); Block\await($promise, $loop, self::TIMEOUT); @@ -259,7 +263,7 @@ public function connectionToHostnameShouldFailImmediately() $promise->then(null, $this->expectCallableOnceWithException( 'InvalidArgumentException', - 'Given URI "tcp://www.google.com:80" does not contain a valid host IP', + 'Given URI "tcp://www.google.com:80" does not contain a valid host IP (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -274,7 +278,7 @@ public function connectionToInvalidPortShouldFailImmediately() $promise->then(null, $this->expectCallableOnceWithException( 'InvalidArgumentException', - 'Given URI "tcp://255.255.255.255:12345678" is invalid', + 'Given URI "tcp://255.255.255.255:12345678" is invalid (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } @@ -332,7 +336,7 @@ public function cancellingConnectionShouldRejectPromise() $this->setExpectedException( 'RuntimeException', - 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake', + 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 ); Block\await($promise, $loop); diff --git a/tests/TcpServerTest.php b/tests/TcpServerTest.php index 204d5680..32a49d28 100644 --- a/tests/TcpServerTest.php +++ b/tests/TcpServerTest.php @@ -319,7 +319,7 @@ public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $ $this->markTestSkipped('not supported on HHVM'); } - $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT), $exception->getMessage()); + $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode()); } diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 3e23543a..81398279 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -36,7 +36,7 @@ public function testRejectsWithTimeoutReasonOnTimeout() $this->setExpectedException( 'RuntimeException', - 'Connection to google.com:80 timed out after 0.01 seconds', + 'Connection to google.com:80 timed out after 0.01 seconds (ETIMEDOUT)', \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 ); Block\await($timeout->connect('google.com:80'), $loop); diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index c6c2e8eb..183c0d3e 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -45,7 +45,7 @@ public function testInvalidScheme() $promise->then(null, $this->expectCallableOnceWithException( 'InvalidArgumentException', - 'Given URI "tcp://google.com:80" is invalid', + 'Given URI "tcp://google.com:80" is invalid (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } diff --git a/tests/UnixServerTest.php b/tests/UnixServerTest.php index a383fe2f..b2d4b59f 100644 --- a/tests/UnixServerTest.php +++ b/tests/UnixServerTest.php @@ -234,7 +234,7 @@ public function testCtorThrowsForInvalidAddressScheme() $this->setExpectedException( 'InvalidArgumentException', - 'Given URI "tcp://localhost:0" is invalid', + 'Given URI "tcp://localhost:0" is invalid (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new UnixServer('tcp://localhost:0', $loop); @@ -328,7 +328,7 @@ public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $ $this->markTestSkipped('not supported on HHVM'); } - $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT), $exception->getMessage()); + $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode()); } From 90d1e0b85bd8bc8659eb45e3e9421ece82803094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Sep 2021 16:39:47 +0200 Subject: [PATCH 6/7] Look up errno based on errstr when listening for connections fails --- src/SocketServer.php | 45 +++++++++++++++++++++++++++-------------- src/TcpServer.php | 6 ++++++ tests/TcpServerTest.php | 20 ++++++++++++++---- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/SocketServer.php b/src/SocketServer.php index c8db84ab..2ea03bae 100644 --- a/src/SocketServer.php +++ b/src/SocketServer.php @@ -113,23 +113,10 @@ public static function accept($socket) // stream_socket_accept(): accept failed: Connection timed out $error = \error_get_last(); $errstr = \preg_replace('#.*: #', '', $error['message']); - - // Go through list of possible error constants to find matching errno. - // @codeCoverageIgnoreStart - $errno = 0; - if (\function_exists('socket_strerror')) { - foreach (\get_defined_constants(false) as $name => $value) { - if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) { - $errno = $value; - $errstr .= ' (' . \substr($name, 7) . ')'; - break; - } - } - } - // @codeCoverageIgnoreEnd + $errno = self::errno($errstr); throw new \RuntimeException( - 'Unable to accept new connection: ' . $errstr, + 'Unable to accept new connection: ' . $errstr . self::errconst($errno), $errno ); } @@ -137,6 +124,34 @@ public static function accept($socket) return $newSocket; } + /** + * [Internal] Returns errno value for given errstr + * + * The errno and errstr values describes the type of error that has been + * encountered. This method tries to look up the given errstr and find a + * matching errno value which can be useful to provide more context to error + * messages. It goes through the list of known errno constants when + * ext-sockets is available to find an errno matching the given errstr. + * + * @param string $errstr + * @return int errno value (e.g. value of `SOCKET_ECONNREFUSED`) or 0 if not found + * @internal + * @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/errno with permission + * @codeCoverageIgnore + */ + public static function errno($errstr) + { + if (\function_exists('socket_strerror')) { + foreach (\get_defined_constants(false) as $name => $value) { + if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) { + return $value; + } + } + } + + return 0; + } + /** * [Internal] Returns errno constant name for given errno value * diff --git a/src/TcpServer.php b/src/TcpServer.php index f5ae09a1..53d5317b 100644 --- a/src/TcpServer.php +++ b/src/TcpServer.php @@ -175,6 +175,12 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a \stream_context_create(array('socket' => $context + array('backlog' => 511))) ); if (false === $this->master) { + if ($errno === 0) { + // PHP does not seem to report errno, so match errno from errstr + // @link https://3v4l.org/3qOBl + $errno = SocketServer::errno($errstr); + } + throw new \RuntimeException( 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno), $errno diff --git a/tests/TcpServerTest.php b/tests/TcpServerTest.php index 32a49d28..b4749cf6 100644 --- a/tests/TcpServerTest.php +++ b/tests/TcpServerTest.php @@ -51,6 +51,7 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() public function testServerEmitsConnectionEventForNewConnection() { $client = stream_socket_client('tcp://localhost:'.$this->port); + assert($client !== false); $server = $this->server; $promise = new Promise(function ($resolve) use ($server) { @@ -70,6 +71,7 @@ public function testConnectionWithManyClients() $client1 = stream_socket_client('tcp://localhost:'.$this->port); $client2 = stream_socket_client('tcp://localhost:'.$this->port); $client3 = stream_socket_client('tcp://localhost:'.$this->port); + assert($client1 !== false && $client2 !== false && $client3 !== false); $this->server->on('connection', $this->expectCallableExactly(3)); $this->tick(); @@ -80,6 +82,7 @@ public function testConnectionWithManyClients() public function testDataEventWillNotBeEmittedWhenClientSendsNoData() { $client = stream_socket_client('tcp://localhost:'.$this->port); + assert($client !== false); $mock = $this->expectCallableNever(); @@ -150,6 +153,7 @@ public function testGetAddressAfterCloseReturnsNull() public function testLoopWillEndWhenServerIsClosedAfterSingleConnection() { $client = stream_socket_client('tcp://localhost:' . $this->port); + assert($client !== false); // explicitly unset server because we only accept a single connection // and then already call close() @@ -203,6 +207,7 @@ public function testDataWillBeEmittedInMultipleChunksWhenClientSendsExcessiveAmo public function testConnectionDoesNotEndWhenClientDoesNotClose() { $client = stream_socket_client('tcp://localhost:'.$this->port); + assert($client !== false); $mock = $this->expectCallableNever(); @@ -236,7 +241,7 @@ public function testCtorAddsResourceToLoop() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addReadStream'); - $server = new TcpServer(0, $loop); + new TcpServer(0, $loop); } public function testResumeWithoutPauseIsNoOp() @@ -316,7 +321,7 @@ public function testEmitsErrorWhenAcceptListenerFails() public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception) { if (defined('HHVM_VERSION')) { - $this->markTestSkipped('not supported on HHVM'); + $this->markTestSkipped('Not supported on HHVM'); } $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage()); @@ -328,9 +333,16 @@ public function testListenOnBusyPortThrows() if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Windows supports listening on same port multiple times'); } + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } - $this->setExpectedException('RuntimeException'); - $another = new TcpServer($this->port, $this->loop); + $this->setExpectedException( + 'RuntimeException', + 'Failed to listen on "tcp://127.0.0.1:' . $this->port . '": ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EADDRINUSE) . ' (EADDRINUSE)' : 'Address already in use'), + defined('SOCKET_EADDRINUSE') ? SOCKET_EADDRINUSE : 0 + ); + new TcpServer($this->port, $this->loop); } /** From 95bce45647dbd9428489e4ead8a0f41b4ddb68f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Sep 2021 17:57:35 +0200 Subject: [PATCH 7/7] Improve error reporting in server examples --- README.md | 1 - examples/01-echo-server.php | 9 ++++++++- examples/02-chat-server.php | 17 +++++++++++++---- examples/03-http-server.php | 17 ++++++++++++++--- examples/91-benchmark-server.php | 15 ++++++++++++--- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3b6ec6d4..3b52482b 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,6 @@ $socket->on('error', function (Exception $e) { Note that this is not a fatal error event, i.e. the server keeps listening for new connections even after this event. - #### getAddress() The `getAddress(): ?string` method can be used to diff --git a/examples/01-echo-server.php b/examples/01-echo-server.php index a690f07a..e073c230 100644 --- a/examples/01-echo-server.php +++ b/examples/01-echo-server.php @@ -19,6 +19,7 @@ // You can also use systemd socket activation and listen on an inherited file descriptor: // // $ systemd-socket-activate -l 8000 php examples/01-echo-server.php php://fd/3 +// $ telnet localhost 8000 require __DIR__ . '/../vendor/autoload.php'; @@ -31,8 +32,14 @@ $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; $connection->pipe($connection); + + $connection->on('close', function () use ($connection) { + echo '[' . $connection->getRemoteAddress() . ' disconnected]' . PHP_EOL; + }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/examples/02-chat-server.php b/examples/02-chat-server.php index 3e2f354c..862a22de 100644 --- a/examples/02-chat-server.php +++ b/examples/02-chat-server.php @@ -19,6 +19,7 @@ // You can also use systemd socket activation and listen on an inherited file descriptor: // // $ systemd-socket-activate -l 8000 php examples/02-chat-server.php php://fd/3 +// $ telnet localhost 8000 require __DIR__ . '/../vendor/autoload.php'; @@ -30,9 +31,11 @@ $socket = new React\Socket\LimitingServer($socket, null); -$socket->on('connection', function (React\Socket\ConnectionInterface $client) use ($socket) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($socket) { + echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; + // whenever a new message comes in - $client->on('data', function ($data) use ($client, $socket) { + $connection->on('data', function ($data) use ($connection, $socket) { // remove any non-word characters (just for the demo) $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data)); @@ -42,13 +45,19 @@ } // prefix with client IP and broadcast to all connected clients - $data = trim(parse_url($client->getRemoteAddress(), PHP_URL_HOST), '[]') . ': ' . $data . PHP_EOL; + $data = trim(parse_url($connection->getRemoteAddress(), PHP_URL_HOST), '[]') . ': ' . $data . PHP_EOL; foreach ($socket->getConnections() as $connection) { $connection->write($data); } }); + + $connection->on('close', function () use ($connection) { + echo '[' . $connection->getRemoteAddress() . ' disconnected]' . PHP_EOL; + }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/examples/03-http-server.php b/examples/03-http-server.php index 09606ab7..dc861a1c 100644 --- a/examples/03-http-server.php +++ b/examples/03-http-server.php @@ -15,14 +15,14 @@ // $ php examples/03-http-server.php 127.0.0.1:8000 // $ curl -v http://localhost:8000/ // $ ab -n1000 -c10 http://localhost:8000/ -// $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 http://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab -n1000 -c10 http://localhost:8000/ // // You can also run a secure HTTPS echo server like this: // // $ php examples/03-http-server.php tls://127.0.0.1:8000 examples/localhost.pem // $ curl -v --insecure https://localhost:8000/ // $ ab -n1000 -c10 https://localhost:8000/ -// $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 https://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab -n1000 -c10 https://localhost:8000/ // // You can also run a Unix domain socket (UDS) server like this: // @@ -32,6 +32,9 @@ // You can also use systemd socket activation and listen on an inherited file descriptor: // // $ systemd-socket-activate -l 8000 php examples/03-http-server.php php://fd/3 +// $ curl -v --insecure https://localhost:8000/ +// $ ab -n1000 -c10 https://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab -n1000 -c10 https://localhost:8000/ require __DIR__ . '/../vendor/autoload.php'; @@ -42,12 +45,20 @@ )); $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; + $connection->once('data', function () use ($connection) { $body = "

Hello world!

\r\n"; $connection->end("HTTP/1.1 200 OK\r\nContent-Length: " . strlen($body) . "\r\nConnection: close\r\n\r\n" . $body); }); + + $connection->on('close', function () use ($connection) { + echo '[' . $connection->getRemoteAddress() . ' disconnected]' . PHP_EOL; + }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . strtr($socket->getAddress(), array('tcp:' => 'http:', 'tls:' => 'https:')) . PHP_EOL; diff --git a/examples/91-benchmark-server.php b/examples/91-benchmark-server.php index 0e3e2025..6a0e7828 100644 --- a/examples/91-benchmark-server.php +++ b/examples/91-benchmark-server.php @@ -21,6 +21,13 @@ // $ php examples/91-benchmark-server.php unix:///tmp/server.sock // $ nc -N -U /tmp/server.sock // $ dd if=/dev/zero bs=1M count=1000 | nc -N -U /tmp/server.sock +// +// You can also use systemd socket activation and listen on an inherited file descriptor: +// +// $ systemd-socket-activate -l 8000 php examples/91-benchmark-server.php php://fd/3 +// $ telnet localhost 8000 +// $ echo hello world | nc -N localhost 8000 +// $ dd if=/dev/zero bs=1M count=1000 | nc -N localhost 8000 require __DIR__ . '/../vendor/autoload.php'; @@ -31,7 +38,7 @@ )); $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { - echo '[connected]' . PHP_EOL; + echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; // count the number of bytes received from this connection $bytes = 0; @@ -43,10 +50,12 @@ $t = microtime(true); $connection->on('close', function () use ($connection, $t, &$bytes) { $t = microtime(true) - $t; - echo '[disconnected after receiving ' . $bytes . ' bytes in ' . round($t, 3) . 's => ' . round($bytes / $t / 1024 / 1024, 1) . ' MiB/s]' . PHP_EOL; + echo '[' . $connection->getRemoteAddress() . ' disconnected after receiving ' . $bytes . ' bytes in ' . round($t, 3) . 's => ' . round($bytes / $t / 1024 / 1024, 1) . ' MiB/s]' . PHP_EOL; }); }); -$socket->on('error', 'printf'); +$socket->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); echo 'Listening on ' . $socket->getAddress() . PHP_EOL;