Skip to content

Commit

Permalink
Trigger E_USER_ERROR on unhandled exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
WyriHaximus committed Jun 28, 2020
1 parent 57d86e5 commit e707f7e
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 15 deletions.
39 changes: 39 additions & 0 deletions src/Internal/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use React\Promise\Promise;
use React\Promise\PromiseInterface;
use Throwable;
use function React\Promise\_checkTypehint;
use function React\Promise\enqueue;
use function React\Promise\fatalError;
Expand All @@ -15,18 +16,54 @@
final class RejectedPromise implements PromiseInterface
{
private $reason;
private $handled = false;

public function __construct(\Throwable $reason)
{
$this->reason = $reason;
}

public function __destruct()
{
if ($this->handled) {
return;
}

$message = 'Unhandled promise rejection with ';

if ($this->reason instanceof Throwable) {
$message .= get_class($this->reason) . ': ' . $this->reason->getMessage();
$message .= ' raised in ' . $this->reason->getFile() . ' on line ' . $this->reason->getLine();
$message .= PHP_EOL . $this->reason->getTraceAsString();
} else {
if ($this->reason === null) {
$message .= 'null';
} else {
$message .= (is_object($this->reason) ? get_class($this->reason) : gettype($this->reason));
}

$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if (isset($trace[0]['file'], $trace[0]['line'])) {
$message .= ' detected in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'];
}

ob_start();
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$message .= PHP_EOL . ob_get_clean();
}

$message .= PHP_EOL;
fatalError($message);
}

public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
{
if (null === $onRejected) {
return $this;
}

$this->handled = true;

return new Promise(function (callable $resolve, callable $reject) use ($onRejected): void {
enqueue(function () use ($resolve, $reject, $onRejected): void {
try {
Expand All @@ -41,6 +78,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
public function done(callable $onFulfilled = null, callable $onRejected = null): void
{
enqueue(function () use ($onRejected) {
$this->handled = true;

if (null === $onRejected) {
return fatalError($this->reason);
}
Expand Down
4 changes: 3 additions & 1 deletion tests/DeferredTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
$deferred = new Deferred(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$deferred->promise()->then(null, function () { });
$deferred->promise()->cancel();
unset($deferred);

Expand All @@ -42,7 +43,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
$deferred = new Deferred(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$deferred->promise()->then()->cancel();
$deferred->promise()->then(null, function () { })->cancel();
unset($deferred);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -56,6 +57,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc

$deferred = new Deferred(function () use (&$deferred) { });
$deferred->reject(new \Exception('foo'));
$deferred->promise()->then(null, function () { });
unset($deferred);

$this->assertSame(0, gc_collect_cycles());
Expand Down
2 changes: 1 addition & 1 deletion tests/FunctionRaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects

$promise2 = new Promise(function () {}, $this->expectCallableNever());

race([$deferred->promise(), $promise2])->cancel();
race([$deferred->promise(), $promise2])->then(null, function () { })->cancel();
}
}
4 changes: 3 additions & 1 deletion tests/FunctionSomeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesRej

$promise2 = new Promise(function () {}, $this->expectCallableNever());

some([$deferred->promise(), $promise2], 2);
$ret = some([$deferred->promise(), $promise2], 2);

$ret->then(null, function () { });
}
}
16 changes: 16 additions & 0 deletions tests/Internal/RejectedPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Exception;
use LogicException;
use React\Promise\ErrorCollector;
use React\Promise\PromiseAdapter\CallbackPromiseAdapter;
use React\Promise\PromiseTest\PromiseRejectedTestTrait;
use React\Promise\PromiseTest\PromiseSettledTestTrait;
Expand Down Expand Up @@ -45,4 +46,19 @@ public function getPromiseTestAdapter(callable $canceller = null)
},
]);
}

/** @test */
public function unhandledRejectionShouldTriggerFatalError()
{
$errorCollector = new ErrorCollector();
$errorCollector->start();

$promise = new RejectedPromise(new Exception('foo'));
unset($promise);

$errors = $errorCollector->stop();

self::assertEquals(E_USER_ERROR, $errors[0]['errno']);
self::assertContains('Unhandled promise rejection with Exception: foo raised in ', $errors[0]['errstr']);
}
}
12 changes: 10 additions & 2 deletions tests/PromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
$promise = new Promise(function () {
throw new \Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -78,6 +79,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
$promise = new Promise(function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -91,6 +93,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
$reject(new \Exception('foo'));
});
$promise->cancel();
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -103,7 +106,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
$reject(new \Exception('foo'));
});
$promise->then()->then()->then()->cancel();
$promise->then()->then()->then(null, function () { })->cancel();
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -116,6 +119,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
$promise = new Promise(function ($resolve, $reject) {
throw new \Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -141,6 +145,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
throw new \Exception('foo');
});
$promise->cancel();
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -157,6 +162,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
$promise = new Promise(function () use (&$promise) {
throw new \Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -173,6 +179,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
$promise = new Promise(function () {
throw new \Exception('foo');
}, function () use (&$promise) { });
$promise->then(null, function () { });
unset($promise);

$this->assertSame(0, gc_collect_cycles());
Expand All @@ -186,7 +193,7 @@ public function shouldIgnoreNotifyAfterReject()
$notify(42);
});

$promise->then(null, null, $this->expectCallableNever());
$promise->then(null, function () { }, $this->expectCallableNever());
$promise->cancel();
}

Expand Down Expand Up @@ -263,6 +270,7 @@ public function shouldFulfillIfFullfilledWithSimplePromise()
$promise = new Promise(function () {
throw new Exception('foo');
});
$promise->then(null, function () { });
unset($promise);

self::assertSame(0, gc_collect_cycles());
Expand Down
14 changes: 6 additions & 8 deletions tests/PromiseTest/CancelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,13 @@ public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows()
/** @test */
public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves()
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->will($this->returnCallback(function ($resolve) {
$resolve();
}));
$once = $this->expectCallableOnce();
$canceller = function ($resolve) use ($once) {
$resolve();
$once();
};

$adapter = $this->getPromiseTestAdapter($mock);
$adapter = $this->getPromiseTestAdapter($canceller);

$adapter->promise()->cancel();
$adapter->promise()->cancel();
Expand Down
9 changes: 8 additions & 1 deletion tests/PromiseTest/PromiseRejectedTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,13 @@ public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchType
$mock = $this->expectCallableNever();

$adapter->reject($exception);
$adapter->promise()
$ret = $adapter->promise()
->otherwise(function (InvalidArgumentException $reason) use ($mock) {
$mock($reason);
});

$ret->then(null, function () { });
$adapter->promise()->then(null, function () { });
}

/** @test */
Expand Down Expand Up @@ -497,6 +500,8 @@ public function cancelShouldReturnNullForRejectedPromise()
$adapter->reject(new Exception());

self::assertNull($adapter->promise()->cancel());

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -507,5 +512,7 @@ public function cancelShouldHaveNoEffectForRejectedPromise()
$adapter->reject(new Exception());

$adapter->promise()->cancel();

$adapter->promise()->then(null, function () { });
}
}
16 changes: 15 additions & 1 deletion tests/PromiseTest/PromiseSettledTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public function thenShouldReturnAPromiseForSettledPromise()

$adapter->settle();
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then());

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -28,6 +30,8 @@ public function thenShouldReturnAllowNullForSettledPromise()

$adapter->settle();
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then(null, null));

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -38,6 +42,8 @@ public function cancelShouldReturnNullForSettledPromise()
$adapter->settle();

self::assertNull($adapter->promise()->cancel());

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -48,6 +54,8 @@ public function cancelShouldHaveNoEffectForSettledPromise()
$adapter->settle();

$adapter->promise()->cancel();

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -57,6 +65,8 @@ public function doneShouldReturnNullForSettledPromise()

$adapter->settle();
self::assertNull($adapter->promise()->done(null, function () {}));

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -66,6 +76,8 @@ public function doneShouldReturnAllowNullForSettledPromise()

$adapter->settle();
self::assertNull($adapter->promise()->done(null, function () {}, null));

$adapter->promise()->then(null, function () { });
}

/** @test */
Expand All @@ -74,6 +86,8 @@ public function alwaysShouldReturnAPromiseForSettledPromise()
$adapter = $this->getPromiseTestAdapter();

$adapter->settle();
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(function () {}));
self::assertInstanceOf(PromiseInterface::class, $ret = $adapter->promise()->always(function () {}));

$ret->then(null, function () { });
}
}

0 comments on commit e707f7e

Please sign in to comment.