Skip to content

Commit ccca668

Browse files
authored
Merge pull request #290 from clue-labs/error-handler
Improve error reporting when custom error handler is used
2 parents aa95c15 + 4227053 commit ccca668

8 files changed

+132
-42
lines changed

src/FdServer.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,21 @@ public function __construct($fd, LoopInterface $loop = null)
8989

9090
$this->loop = $loop ?: Loop::get();
9191

92-
$this->master = @\fopen('php://fd/' . $fd, 'r+');
93-
if (false === $this->master) {
92+
$errno = 0;
93+
$errstr = '';
94+
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
9495
// Match errstr from PHP's warning message.
9596
// fopen(php://fd/3): Failed to open stream: Error duping file descriptor 3; possibly it doesn't exist: [9]: Bad file descriptor
96-
$error = \error_get_last();
97-
\preg_match('/\[(\d+)\]: (.*)/', $error['message'], $m);
97+
\preg_match('/\[(\d+)\]: (.*)/', $error, $m);
9898
$errno = isset($m[1]) ? (int) $m[1] : 0;
99-
$errstr = isset($m[2]) ? $m[2] : $error['message'];
99+
$errstr = isset($m[2]) ? $m[2] : $error;
100+
});
101+
102+
$this->master = \fopen('php://fd/' . $fd, 'r+');
100103

104+
\restore_error_handler();
105+
106+
if (false === $this->master) {
101107
throw new \RuntimeException(
102108
'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno),
103109
$errno

src/SocketServer.php

+11-6
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,20 @@ public function close()
106106
*/
107107
public static function accept($socket)
108108
{
109-
$newSocket = @\stream_socket_accept($socket, 0);
110-
111-
if (false === $newSocket) {
109+
$errno = 0;
110+
$errstr = '';
111+
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
112112
// Match errstr from PHP's warning message.
113113
// stream_socket_accept(): accept failed: Connection timed out
114-
$error = \error_get_last();
115-
$errstr = \preg_replace('#.*: #', '', $error['message']);
116-
$errno = self::errno($errstr);
114+
$errstr = \preg_replace('#.*: #', '', $error);
115+
$errno = SocketServer::errno($errstr);
116+
});
117117

118+
$newSocket = \stream_socket_accept($socket, 0);
119+
120+
\restore_error_handler();
121+
122+
if (false === $newSocket) {
118123
throw new \RuntimeException(
119124
'Unable to accept new connection: ' . $errstr . self::errconst($errno),
120125
$errno

src/TcpConnector.php

+13-7
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,19 @@ public function connect($uri)
116116
// Linux reports socket errno and errstr again when trying to write to the dead socket.
117117
// Suppress error reporting to get error message below and close dead socket before rejecting.
118118
// This is only known to work on Linux, Mac and Windows are known to not support this.
119-
@\fwrite($stream, \PHP_EOL);
120-
$error = \error_get_last();
121-
122-
// fwrite(): send of 2 bytes failed with errno=111 Connection refused
123-
\preg_match('/errno=(\d+) (.+)/', $error['message'], $m);
124-
$errno = isset($m[1]) ? (int) $m[1] : 0;
125-
$errstr = isset($m[2]) ? $m[2] : $error['message'];
119+
$errno = 0;
120+
$errstr = '';
121+
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
122+
// Match errstr from PHP's warning message.
123+
// fwrite(): send of 1 bytes failed with errno=111 Connection refused
124+
\preg_match('/errno=(\d+) (.+)/', $error, $m);
125+
$errno = isset($m[1]) ? (int) $m[1] : 0;
126+
$errstr = isset($m[2]) ? $m[2] : $error;
127+
});
128+
129+
\fwrite($stream, \PHP_EOL);
130+
131+
\restore_error_handler();
126132
} else {
127133
// Not on Linux and ext-sockets not available? Too bad.
128134
$errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;

src/UnixServer.php

+16-12
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,29 @@ public function __construct($path, LoopInterface $loop = null, array $context =
6363
);
6464
}
6565

66-
$this->master = @\stream_socket_server(
66+
$errno = 0;
67+
$errstr = '';
68+
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
69+
// PHP does not seem to report errno/errstr for Unix domain sockets (UDS) right now.
70+
// This only applies to UDS server sockets, see also https://3v4l.org/NAhpr.
71+
// Parse PHP warning message containing unknown error, HHVM reports proper info at least.
72+
if (\preg_match('/\(([^\)]+)\)|\[(\d+)\]: (.*)/', $error, $match)) {
73+
$errstr = isset($match[3]) ? $match['3'] : $match[1];
74+
$errno = isset($match[2]) ? (int)$match[2] : 0;
75+
}
76+
});
77+
78+
$this->master = \stream_socket_server(
6779
$path,
6880
$errno,
6981
$errstr,
7082
\STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
7183
\stream_context_create(array('socket' => $context))
7284
);
73-
if (false === $this->master) {
74-
// PHP does not seem to report errno/errstr for Unix domain sockets (UDS) right now.
75-
// This only applies to UDS server sockets, see also https://3v4l.org/NAhpr.
76-
// Parse PHP warning message containing unknown error, HHVM reports proper info at least.
77-
if ($errno === 0 && $errstr === '') {
78-
$error = \error_get_last();
79-
if (\preg_match('/\(([^\)]+)\)|\[(\d+)\]: (.*)/', $error['message'], $match)) {
80-
$errstr = isset($match[3]) ? $match['3'] : $match[1];
81-
$errno = isset($match[2]) ? (int)$match[2] : 0;
82-
}
83-
}
8485

86+
\restore_error_handler();
87+
88+
if (false === $this->master) {
8589
throw new \RuntimeException(
8690
'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno),
8791
$errno

tests/FdServerTest.php

+27-4
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function testCtorThrowsForInvalidUrl()
5151
new FdServer('tcp://127.0.0.1:8080', $loop);
5252
}
5353

54-
public function testCtorThrowsForUnknownFd()
54+
public function testCtorThrowsForUnknownFdWithoutCallingCustomErrorHandler()
5555
{
5656
if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
5757
$this->markTestSkipped('Not supported on your platform');
@@ -62,12 +62,27 @@ public function testCtorThrowsForUnknownFd()
6262
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
6363
$loop->expects($this->never())->method('addReadStream');
6464

65+
$error = null;
66+
set_error_handler(function ($_, $errstr) use (&$error) {
67+
$error = $errstr;
68+
});
69+
6570
$this->setExpectedException(
6671
'RuntimeException',
6772
'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EBADF) . ' (EBADF)' : 'Bad file descriptor'),
6873
defined('SOCKET_EBADF') ? SOCKET_EBADF : 9
6974
);
70-
new FdServer($fd, $loop);
75+
76+
try {
77+
new FdServer($fd, $loop);
78+
79+
restore_error_handler();
80+
} catch (\Exception $e) {
81+
restore_error_handler();
82+
$this->assertNull($error);
83+
84+
throw $e;
85+
}
7186
}
7287

7388
public function testCtorThrowsIfFdIsAFileAndNotASocket()
@@ -319,7 +334,7 @@ public function testServerEmitsConnectionEventForNewConnection()
319334
$server->close();
320335
}
321336

322-
public function testEmitsErrorWhenAcceptListenerFails()
337+
public function testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler()
323338
{
324339
if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
325340
$this->markTestSkipped('Not supported on your platform');
@@ -346,10 +361,18 @@ public function testEmitsErrorWhenAcceptListenerFails()
346361
$this->assertNotNull($listener);
347362
$socket = stream_socket_server('tcp://127.0.0.1:0');
348363

364+
$error = null;
365+
set_error_handler(function ($_, $errstr) use (&$error) {
366+
$error = $errstr;
367+
});
368+
349369
$time = microtime(true);
350370
$listener($socket);
351371
$time = microtime(true) - $time;
352372

373+
restore_error_handler();
374+
$this->assertNull($error);
375+
353376
$this->assertLessThan(1, $time);
354377

355378
$this->assertInstanceOf('RuntimeException', $exception);
@@ -362,7 +385,7 @@ public function testEmitsErrorWhenAcceptListenerFails()
362385
/**
363386
* @param \RuntimeException $e
364387
* @requires extension sockets
365-
* @depends testEmitsErrorWhenAcceptListenerFails
388+
* @depends testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler
366389
*/
367390
public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception)
368391
{

tests/TcpConnectorTest.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,32 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
2525
}
2626

2727
/** @test */
28-
public function connectionToEmptyPortShouldFail()
28+
public function connectionToEmptyPortShouldFailWithoutCallingCustomErrorHandler()
2929
{
3030
$connector = new TcpConnector();
3131
$promise = $connector->connect('127.0.0.1:9999');
3232

33+
$error = null;
34+
set_error_handler(function ($_, $errstr) use (&$error) {
35+
$error = $errstr;
36+
});
37+
3338
$this->setExpectedException(
3439
'RuntimeException',
3540
'Connection to tcp://127.0.0.1:9999 failed: Connection refused' . (function_exists('socket_import_stream') ? ' (ECONNREFUSED)' : ''),
3641
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
3742
);
38-
Block\await($promise, null, self::TIMEOUT);
43+
44+
try {
45+
Block\await($promise, null, self::TIMEOUT);
46+
47+
restore_error_handler();
48+
} catch (\Exception $e) {
49+
restore_error_handler();
50+
$this->assertNull($error);
51+
52+
throw $e;
53+
}
3954
}
4055

4156
/** @test */

tests/TcpServerTest.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ public function testCloseRemovesResourceFromLoop()
276276
$server->close();
277277
}
278278

279-
public function testEmitsErrorWhenAcceptListenerFails()
279+
public function testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler()
280280
{
281281
$listener = null;
282282
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
@@ -295,10 +295,18 @@ public function testEmitsErrorWhenAcceptListenerFails()
295295
$this->assertNotNull($listener);
296296
$socket = stream_socket_server('tcp://127.0.0.1:0');
297297

298+
$error = null;
299+
set_error_handler(function ($_, $errstr) use (&$error) {
300+
$error = $errstr;
301+
});
302+
298303
$time = microtime(true);
299304
$listener($socket);
300305
$time = microtime(true) - $time;
301306

307+
restore_error_handler();
308+
$this->assertNull($error);
309+
302310
$this->assertLessThan(1, $time);
303311

304312
$this->assertInstanceOf('RuntimeException', $exception);
@@ -311,7 +319,7 @@ public function testEmitsErrorWhenAcceptListenerFails()
311319
/**
312320
* @param \RuntimeException $e
313321
* @requires extension sockets
314-
* @depends testEmitsErrorWhenAcceptListenerFails
322+
* @depends testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler
315323
*/
316324
public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception)
317325
{

tests/UnixServerTest.php

+27-4
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,27 @@ public function testCtorThrowsForInvalidAddressScheme()
240240
new UnixServer('tcp://localhost:0', $loop);
241241
}
242242

243-
public function testCtorThrowsWhenPathIsNotWritable()
243+
public function testCtorThrowsWhenPathIsNotWritableWithoutCallingCustomErrorHandler()
244244
{
245245
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
246246

247+
$error = null;
248+
set_error_handler(function ($_, $errstr) use (&$error) {
249+
$error = $errstr;
250+
});
251+
247252
$this->setExpectedException('RuntimeException');
248-
$server = new UnixServer('/dev/null', $loop);
253+
254+
try {
255+
new UnixServer('/dev/null', $loop);
256+
257+
restore_error_handler();
258+
} catch (\Exception $e) {
259+
restore_error_handler();
260+
$this->assertNull($error);
261+
262+
throw $e;
263+
}
249264
}
250265

251266
public function testResumeWithoutPauseIsNoOp()
@@ -285,7 +300,7 @@ public function testCloseRemovesResourceFromLoop()
285300
$server->close();
286301
}
287302

288-
public function testEmitsErrorWhenAcceptListenerFails()
303+
public function testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler()
289304
{
290305
$listener = null;
291306
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
@@ -304,10 +319,18 @@ public function testEmitsErrorWhenAcceptListenerFails()
304319
$this->assertNotNull($listener);
305320
$socket = stream_socket_server('tcp://127.0.0.1:0');
306321

322+
$error = null;
323+
set_error_handler(function ($_, $errstr) use (&$error) {
324+
$error = $errstr;
325+
});
326+
307327
$time = microtime(true);
308328
$listener($socket);
309329
$time = microtime(true) - $time;
310330

331+
restore_error_handler();
332+
$this->assertNull($error);
333+
311334
$this->assertLessThan(1, $time);
312335

313336
$this->assertInstanceOf('RuntimeException', $exception);
@@ -320,7 +343,7 @@ public function testEmitsErrorWhenAcceptListenerFails()
320343
/**
321344
* @param \RuntimeException $e
322345
* @requires extension sockets
323-
* @depends testEmitsErrorWhenAcceptListenerFails
346+
* @depends testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler
324347
*/
325348
public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception)
326349
{

0 commit comments

Comments
 (0)