From f4cda31b2137830021bad8e17f30a70617b8d478 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Mon, 9 Feb 2026 16:06:43 +0100 Subject: [PATCH 1/2] Fix STDERR pollution corrupting exception serialization and silent exception swallowing When PHP emits deprecation notices or warnings to STDERR during child task execution, the serialized exception data on the same stream gets corrupted, causing the parent to fall back to an empty ParallelError. Fix by using a unique delimiter marker so the parent can extract the serialized data regardless of preceding noise. Also fix a bug where exceptions were silently swallowed when catch handlers were registered but none matched the thrown exception type. Fixes #254 Co-Authored-By: Claude Opus 4.6 --- src/Process/ParallelProcess.php | 7 +++++++ src/Process/ProcessCallbacks.php | 4 +++- src/Runtime/ChildRuntime.php | 2 +- tests/Feature/ErrorHandlingTest.php | 31 +++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Process/ParallelProcess.php b/src/Process/ParallelProcess.php index c27155c..99f03cd 100644 --- a/src/Process/ParallelProcess.php +++ b/src/Process/ParallelProcess.php @@ -87,6 +87,13 @@ public function getErrorOutput() if (! $this->errorOutput) { $processOutput = $this->process->getErrorOutput(); + $marker = '___SPATIE_ASYNC_CHILD___'; + $markerPosition = strrpos($processOutput, $marker); + + if ($markerPosition !== false) { + $processOutput = substr($processOutput, $markerPosition + strlen($marker)); + } + $childResult = @unserialize(base64_decode($processOutput)); if ($childResult === false || ! array_key_exists('output', $childResult)) { diff --git a/src/Process/ProcessCallbacks.php b/src/Process/ProcessCallbacks.php index 52f9945..7780e70 100644 --- a/src/Process/ProcessCallbacks.php +++ b/src/Process/ProcessCallbacks.php @@ -64,8 +64,10 @@ public function triggerError() call_user_func_array($callback, [$exception]); - break; + return; } + + throw $exception; } abstract protected function resolveErrorOutput(): Throwable; diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index e0cbc4a..fa36db9 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -51,7 +51,7 @@ $output = new \Spatie\Async\Output\SerializableException($exception); - fwrite(STDERR, base64_encode(serialize(['output' => $output]))); + fwrite(STDERR, '___SPATIE_ASYNC_CHILD___'.base64_encode(serialize(['output' => $output]))); exit(1); } diff --git a/tests/Feature/ErrorHandlingTest.php b/tests/Feature/ErrorHandlingTest.php index b7ba7ad..a304c12 100644 --- a/tests/Feature/ErrorHandlingTest.php +++ b/tests/Feature/ErrorHandlingTest.php @@ -185,6 +185,37 @@ expect($caughtError)->toBeInstanceOf(ParseError::class); }); +it('preserves exception type when stderr contains noise', function () { + $pool = Pool::create(); + + $caughtException = null; + + $pool->add(childTask(function () { + trigger_error('some deprecation notice', E_USER_WARNING); + + throw new MyException('test'); + }))->catch(function (MyException $e) use (&$caughtException) { + $caughtException = $e; + }); + + $pool->wait(); + + expect($caughtException)->toBeInstanceOf(MyException::class); + expect($caughtException->getMessage())->toContain('test'); +}); + +it('throws exception when no catch handler matches', function () { + $pool = Pool::create(); + + $pool->add(childTask(function () { + throw new MyException('test'); + }))->catch(function (OtherException $e) { + // This handler should not match + }); + + $pool->wait(); +})->throws(MyException::class); + it('can handle synchronous exception', function () { Pool::$forceSynchronous = true; From 34d2eaa61e7ec4862fc6d540116d376444e4fc88 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Mon, 9 Feb 2026 15:07:06 +0000 Subject: [PATCH 2/2] Fix styling --- tests/Feature/PoolTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Feature/PoolTest.php b/tests/Feature/PoolTest.php index 93ef1ab..dc98f99 100644 --- a/tests/Feature/PoolTest.php +++ b/tests/Feature/PoolTest.php @@ -336,7 +336,9 @@ $cntTasks = 30; foreach (range(1, $cntTasks) as $i) { - $pool->add(childTask(function () { return 1; })); + $pool->add(childTask(function () { + return 1; + })); } $pool->wait();