Skip to content

Breaking Change: set_exception_handler no longer returns the exception handler within itself #13446

Closed

Description

@greg0ire

Description

The following code:

<?php
set_exception_handler(function ($e): void {
    var_dump(set_exception_handler('is_int'));
});
throw new \Exception;

Resulted in this output:

NULL

But I expected this output instead:

object(Closure)#1 (1) {
  ["parameter"]=>
  array(1) {
    ["$e"]=>
    string(10) "<required>"
  }
}

PHP Version

8.3.0 and up

Operating System

No response

Activity

  1. ndossche commented on Feb 20, 2024

    @ndossche
    Member

    Reverting the change in zend_user_exception_handler from b3e33be seems to restore the old behaviour.
    cc @iluuu1994 is this behaviour expected, based on what I remember and read it seems unintended but I could be wrong?

  2. iluuu1994 commented on Feb 20, 2024

    @iluuu1994
    Member

    Yes, this is expected behavior. Exception handlers are not called repeatedly, so they are unregistered before they are called. If you want to catch errors within an error handler, install a new one. You should probably avoid registering the same one, as this will lead to infinite recursion if there's a bug in your error handler. This was previously not done because errors in your error handler were never handled.

  3. ndossche commented on Feb 21, 2024

    @ndossche
    Member

    Ok thanks for checking. In that case I'll close this.

  4. greg0ire commented on Feb 21, 2024

    @greg0ire
    ContributorAuthor

    Note that this also affects shutdown functions: https://3v4l.org/fXeHc#v8.3.3, and I think that's what really causes symfony/symfony#53946: my understanding is that with the shutdown function, Symfony is trying to reuse its exception handler to handle fatal errors.

  5. ndossche commented on Feb 21, 2024

    @ndossche
    Member

    FTR, the behaviour change was to implement #10695, but I see how this causes issues.
    Not sure what to do with this, if the existing code could still be supported, I'll defer to @iluuu1994 as they're more familiar

  6. greg0ire commented on Feb 21, 2024

    @greg0ire
    ContributorAuthor

    I think it makes sense to unregister the exception handler when you are inside it, but maybe it should be restored after exiting it, and before entering the shutdown function. Doesn't make a lot of sense though… when I said Symfony is reusing the exception handler, it's not like they are throwing an exception and letting the handler handle it… instead, they are invoking it manually. So I'd say it's not a super legitimate use case. This looks like a breaking change though.

  7. nicolas-grekas commented on Feb 23, 2024

    @nicolas-grekas
    Contributor

    Hi, this is a significant BC break, please reconsider. See symfony/symfony#53946, which is nasty.

  8. iluuu1994 commented on Feb 27, 2024

    @iluuu1994
    Member

    @nicolas-grekas

    So, just so I understand this correctly, ErrorHandler::handleFatalError depends on finding itself in the error handler stack. Because calling the current exception handler also removes it as the current exception handler, it cannot be recovered with the current API, as set_exception_handler('var_dump') immediately returns null. TBH the $handler = set_exception_handler('var_dump') and restore_exception_handler() pattern is probably not what this API was meant for. Nonetheless, let's find a way this continues working.

    The reason the exception handler is removed is to avoid infinite recursion when an exception handler accidentally throws. Another way to solve this is to only re-trigger the exception handler only if a new one has been registered. It's worth noting that your code is potentially subject to infinite recursion with that definition, as you push/pop exception handlers to iterate the stack. This was not previously the case because registering exception handlers within exception handlers had no effect, as they would never be called anyway.

    A more sturdy solution might be to offer better APIs for the underlying error handler stack (EG(user_exception_handlers)).

    I'm not fully grasping the Symfony code, e.g. I don't understand why handleFatalError is not just an instance method that would make it unnecessary to find the correct instance within the handler stack. That just to say that my understanding of the issue may be incomplete.

  9. greg0ire commented on Feb 27, 2024

    @greg0ire
    ContributorAuthor

    I don't understand why handleFatalError is not just an instance method

    I don't know either, but in case it helps remembering/understanding, that was originally the case, and changed in symfony/symfony#10921

  10. nicolas-grekas commented on Mar 7, 2024

    @nicolas-grekas
    Contributor

    I think it makes sense to unregister the exception handler when you are inside it, but maybe it should be restored after exiting it

    Yes, that's the real problem for Symfony: when an exception is handled, the exception handler stack is emptied after the userland exception handlers ran.

    I'd say it's not a super legitimate use case.

    Yes it is. This code in the shutdown function turns fatal errors into fake exceptions that are then reinjected in the exception handling stack. Most fatal errors have been turned into exceptions in the recent years, but some remain, and this logic makes the behavior more consistent. That's why we need access to the exception handlers stack at shutdown.

    This is the reproducer:

    set_exception_handler(function ($e) {
        echo $e->getMessage(), "\n";
    });
    
    register_shutdown_function(function (): void {
        var_dump(set_exception_handler('var_dump'));
    });
    
    (function (int $toto): void { echo $toto; })('toto');
  11. greg0ire commented on Mar 7, 2024

    @greg0ire
    ContributorAuthor

    I'd say it's not a super legitimate use case.

    Sorry, I didn't use the right words. I meant to say that although there is a need for sure, the fact that it is achieved by calling the exception handler manually makes it easy to overlook and break by changes in the language, and might point to a shortcoming in PHP. Maybe PHP should handle exceptions thrown inside the shutdown function, so that there is no need to handle inconsistencies in userland?

  12. nicolas-grekas commented on Mar 12, 2024

    @nicolas-grekas
    Contributor

    That's fine, error handling is an edgy domain anyway.

    it makes sense to unregister the exception handler when you are inside it, but maybe it should be restored after exiting it

    That's the fix I'm looking for :)

  13. added a commit that references this issue on Mar 12, 2024
  14. iluuu1994 commented on Mar 12, 2024

    @iluuu1994
    Member

    @nicolas-grekas I'll have another look today. I was thinking that maybe it makes sense to push a null handler during the duration of the handler. This would avoid triggering it recursively, while keeping the current handler available in the stack. After the handler finishes, we can pop the null handler again. Potentially, some additional rules are necessary to avoid popping the null value if the hander has modified the handler stack, i.e. if the top isn't null anymore. I'll need to think about this in a bit more detail. WDYT?

  15. added a commit that references this issue on Mar 19, 2024
    3d30bd0
  16. added a commit that references this issue on Mar 20, 2024
    3301d96
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions