[php-src] Issue #24491: Stream error_handler given as 'Class::method' turns the real exception into a TypeError
| From: | dev-null-undefined | Date: | Wed, 21 Oct 2026 18:00:42 +0000 |
| Subject: | [php-src] Issue #24491: Stream error_handler given as 'Class::method' turns the real exception into a TypeError | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253058@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24491
Author: dev-null-undefined
### Description
Since 2f7b01b86c, if a stream context's error_handler is a static method given as a string or
an array, the exception that is already pending gets replaced by a TypeError about
self/parent/static. That pending exception is the StreamException in exception mode, or whatever a
user error handler threw. The original exception is only reachable through getPrevious(), so
catch (StreamException) no longer catches it. Closures work fine.
The following code:
```php
<?php
class H { static function handle($errors) { echo "handler called\n"; } }
function run(string $label, array $opts) {
try {
fopen('/nonexistent/dir/file', 'r', false,
stream_context_create(['stream' => $opts]));
} catch (Throwable $e) {
echo "$label: ", $e::class, ": ", $e->getMessage(), "\n";
if ($p = $e->getPrevious()) echo " previous: ", $p::class, "\n";
}
}
run('exception mode', ['error_mode' => StreamErrorMode::Exception,
'error_handler' => 'H::handle']);
set_error_handler(fn($no, $str) => throw new ErrorException($str));
run('error handler', ['error_handler' => ['H',
'handle']]);
```
Resulted in this output:
```
exception mode: TypeError: stream error handler must be a valid callback, cannot use
"self"/"parent"/"static" in callables
previous: StreamException
error handler: TypeError: stream error handler must be a valid callback, cannot use
"self"/"parent"/"static" in callables
previous: ErrorException
```
But I expected this output instead:
```
exception mode: StreamException: Failed to open stream: No such file or directory
error handler: ErrorException: fopen(): Failed to open stream: No such file or directory
```
zend_is_callable_check_class() gives up when EG(exception) is already set, and since that commit it
fills in the self/parent/static message. php_stream_call_error_handler() now throws it
unconditionally. Skipping the handler when an exception is already pending, or only throwing when
there isn't one, would avoid this.
The same commit also introduced a small leak. With a throwing error handler,
register_shutdown_function(['self', 'foo']) inside a class leaks
the error string: zend_parse_arg() returns FAILURE on EG(exception) without freeing it. A debug
build reports zend_API.c(3835) : Freeing ... (49 bytes).
### PHP Version
```
PHP 8.7.0-dev (cli) (built: Oct 21 2026 18:50:46) (NTS DEBUG)
```
### Operating System
Linux x86_64