From b0668bfe90f33c401551d6054fc456c5a2d32d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 23 May 2024 13:26:04 +0200 Subject: [PATCH] Include previous exceptions when reporting unhandled promise rejections --- src/Internal/RejectedPromise.php | 8 ++--- ...ReportUnhandledWithPreviousExceptions.phpt | 27 +++++++++++++++ ...ReportUnhandledForTypeErrorOnlyOnPhp7.phpt | 2 +- ...ReportUnhandledForTypeErrorOnlyOnPhp8.phpt | 2 +- ...ramForUnhandledWithPreviousExceptions.phpt | 34 +++++++++++++++++++ 5 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt create mode 100644 tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt diff --git a/src/Internal/RejectedPromise.php b/src/Internal/RejectedPromise.php index 5c4f7be..aa1dff3 100644 --- a/src/Internal/RejectedPromise.php +++ b/src/Internal/RejectedPromise.php @@ -37,8 +37,7 @@ public function __destruct() $handler = set_rejection_handler(null); if ($handler === null) { - $message = 'Unhandled promise rejection with ' . \get_class($this->reason) . ': ' . $this->reason->getMessage() . ' in ' . $this->reason->getFile() . ':' . $this->reason->getLine() . PHP_EOL; - $message .= 'Stack trace:' . PHP_EOL . $this->reason->getTraceAsString(); + $message = 'Unhandled promise rejection with ' . $this->reason; \error_log($message); return; @@ -47,8 +46,9 @@ public function __destruct() try { $handler($this->reason); } catch (\Throwable $e) { - $message = 'Fatal error: Uncaught ' . \get_class($e) . ' from unhandled promise rejection handler: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . PHP_EOL; - $message .= 'Stack trace:' . PHP_EOL . $e->getTraceAsString(); + \preg_match('/^([^:\s]++)(.*+)$/sm', (string) $e, $match); + \assert(isset($match[1], $match[2])); + $message = 'Fatal error: Uncaught ' . $match[1] . ' from unhandled promise rejection handler' . $match[2]; \error_log($message); exit(255); diff --git a/tests/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt b/tests/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt new file mode 100644 index 0000000..74cf4d9 --- /dev/null +++ b/tests/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt @@ -0,0 +1,27 @@ +--TEST-- +Calling reject() without any handlers should report unhandled rejection with all previous exceptions +--INI-- +# suppress legacy PHPUnit 7 warning for Xdebug 3 +xdebug.default_enable= +--FILE-- + +--EXPECTF-- +Unhandled promise rejection with InvalidArgumentException in %s:%d +Stack trace: +#0 %A{main} + +Next OverflowException: bar in %s:%d +Stack trace: +#0 %A{main} + +Next RuntimeException: foo in %s:%d +Stack trace: +#0 %A{main} diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt index d4880ce..bbf01c4 100644 --- a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt +++ b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt @@ -18,7 +18,7 @@ reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueExcepti ?> --EXPECTF-- -Unhandled promise rejection with TypeError: Argument 1 passed to {closure}() must be an instance of UnexpectedValueException, instance of RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d in %s:%d +Unhandled promise rejection with TypeError: Argument 1 passed to {closure}() must be an instance of UnexpectedValueException, instance of RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d and defined in %s:%d Stack trace: #0 %s/src/Internal/RejectedPromise.php(%d): {closure}(%S) #1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S) diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt index aa56786..698f7ec 100644 --- a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt +++ b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt @@ -18,7 +18,7 @@ reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueExcepti ?> --EXPECTF-- -Unhandled promise rejection with TypeError: {closure%S}(): Argument #1 ($unexpected) must be of type UnexpectedValueException, RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d in %s:%d +Unhandled promise rejection with TypeError: {closure%S}(): Argument #1 ($unexpected) must be of type UnexpectedValueException, RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d and defined in %s:%d Stack trace: #0 %s/src/Internal/RejectedPromise.php(%d): {closure%S}(%S) #1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S) diff --git a/tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt b/tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt new file mode 100644 index 0000000..0914975 --- /dev/null +++ b/tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt @@ -0,0 +1,34 @@ +--TEST-- +The callback given to set_rejection_handler() should not throw an exception or the program should terminate for unhandled rejection with all previous exceptions +--INI-- +# suppress legacy PHPUnit 7 warning for Xdebug 3 +xdebug.default_enable= +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught InvalidArgumentException from unhandled promise rejection handler in %s:%d +Stack trace: +#0 %A{main} + +Next OverflowException: bar in %s:%d +Stack trace: +#0 %A{main} + +Next RuntimeException: foo in %s:%d +Stack trace: +#0 %A{main}