Search code examples
javascriptreactjsrecursionprototype

Hijacking addEventListener results in too much recursion when adding a react-invokeguardedcallback listener to HTMLUnknownElement


I have a use case where I'd like to monitor event listeners being added. I found a method to do this that worked for me here: https://stackoverflow.com/a/6434924

This code seems to work in many situations, but, it would seem it will throw a InternalError: too much recursion exception when used on a page containing a react app.

Simplest reproducer looks like this:

Node.prototype.realAddEventListener = Node.prototype.addEventListener;

Node.prototype.addEventListener = function(a,b,c){
    this.realAddEventListener(a,b,c);
}

Printing this , a, and b just before the error happens yields:

this = [object HTMLUnknownElement] 

a = react-invokeguardedcallback 

b = function callCallback() {
        // We immediately remove the callback from event listeners so that
        // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
        // nested call would trigger the fake event handlers of any call higher
        // in the stack.
        fakeNode.removeEventListener(evtType, callCallback, false); // We check for window.hasOwnProperty('event') to prevent the
        // window.event assignment in both IE <= 10 as they throw an error
        // "Member not found" in strict mode, and in Firefox which does not
        // support window.event.

        if (typeof window.event !== 'undefined' && window.hasOwnProperty('event')) {
          window.event = windowEvent;
        }

        func.apply(context, funcArgs);
        didError = false;
      }

What is it about adding a listener to an HTMLUnknownElement on the react-invokeguardedcallback with the function handler above that causes my hijacked addEventListener to recurse?


Solution

  • Thanks @Bergi and @Ivan Castellanos for the reality check.

    This behavior was caused by the development environment. The original code in the post works fine under normal circumstances.

    However, if the source files are being watched and hot-reloaded on changes, and said source files are also injected into the page by a web extension content script (using Method 1 described here: https://stackoverflow.com/a/9517879/2041427), the InternalError: too much recursion will occur.

    This happens because the addEventListener function would have already been swapped in the page script context when the old version of the file was injected into the page. Upon hot-reload addEventListener equals

    function(a,b,c){
        this.realAddEventListener(a,b,c)
    }
    

    Which is then set to Node.prototype.realAddEventListener causing an infinite recursive loop.

    The old ''fix'' below ''worked'' because, on hot-reload+re-injection the error: Uncaught SyntaxError: redeclaration of const handlerMagic would prevent the code from running again.

    The actual fix is to only assign Node.prototype.realAddEventListener if Node.prototype.realAddEventListener is undefined, like so:

    if(!Node.prototype.realAddEventListener){
        Node.prototype.realAddEventListener = Node.prototype.addEventListener
    }
    
    Node.prototype.addEventListener = function(a,b,c){
        this.realAddEventListener
    }
    

    Fixed it.

    Turns out realAddEventListener was not in getting assigned to the original addEventListener implementation. Instead, my re-assignment of addEventListener happened first, and then realAddEventListener was assigned to my hijacked function causing infinite recursion.

    The fix was to ensure the order of events with a Promise:

    
    const handlerMagic = new Promise((resolve)=>{
        Node.prototype.realAddEventListener = Node.prototype.addEventListener;
        resolve()
    }).then(_=>{
        Node.prototype.addEventListener = function(a,b,c){
            this.realAddEventListener(a,b,c)
        }
    })
    
    

    Works fine now.