JDK-8315915 : NPE thrown for event listeners after reloading page in a WebView
  • Type: Bug
  • Component: javafx
  • Sub-Component: web
  • Affected Version:
    jfx11.0.17,8u351,jfx17.0.5,jfx19.0.1,jfx20,jfx21 jfx11.0.17,8u351,jfx17.0.5,jfx19.0.1,jfx20,jfx21
  • Priority: P3
  • Status: Closed
  • Resolution: Not an Issue
  • OS: generic
  • CPU: generic
  • Submitted: 2023-09-08
  • Updated: 2023-09-14
  • Resolved: 2023-09-14
Related Reports
Relates :  
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
OS:
Linux Mint 21.2 Cinnamon

Java: 
openjdk version "20.0.2" 2023-07-18
OpenJDK Runtime Environment Temurin-20.0.2+9 (build 20.0.2+9)
OpenJDK 64-Bit Server VM Temurin-20.0.2+9 (build 20.0.2+9, mixed mode, sharing)

A DESCRIPTION OF THE PROBLEM :
On JavaFX 19 and 20, I cannot reliably register an EventListener multiple times on a WebView when reloading the page. Specifically if I unload the page (by loading "about:blank"), wait for a time, then reload the page again, I cannot reregister the same EventListener instance without breaking event handling.

If the GC happens to run during the time the page is unloaded, then after the page is loaded again and an event occurs, a NullPointerException is logged and my EventListener is not called.

In the attached example, the important details are:
1. A page is loaded into a WebView, and when the status becomes SUCCEEDED, an event listener is registered for the button on the page.
2. After the page's button is clicked, we unload the page by loading "about:blank". Then we request a GC cycle.
  - The GC cycle is no something I do in real world code, but do it here for the sake of reproduction.
3. When we load the page again, we register the same EventListener instance.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
- Run the attached example.
- Click the "Open Page" button to open a web page in the WebView with a single button in it.
- Click the "Close Page" button in the WebView to close the page (load about:blank)
- Click the "Open Page" button again to reopen the web page.
- Click the "Close Page" button again.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The EventListener should be called and the "about:blank" page loaded into the WebView.
ACTUAL -
The EventListener is not invoked, and this NullPointerException is logged:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
	at javafx.web@20/com.sun.webkit.WebPage.twkProcessMouseEvent(Native Method)
	at javafx.web@20/com.sun.webkit.WebPage.dispatchMouseEvent(WebPage.java:857)
	at javafx.web@20/javafx.scene.web.WebView.processMouseEvent(WebView.java:1098)
	at javafx.web@20/javafx.scene.web.WebView.lambda$registerEventHandlers$3(WebView.java:1224)
	at javafx.base@20/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)
	at javafx.base@20/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
	at javafx.base@20/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232)
	at javafx.base@20/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189)
	at javafx.base@20/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
	at javafx.base@20/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
	at javafx.base@20/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at javafx.base@20/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
	at javafx.base@20/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at javafx.base@20/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
	at javafx.base@20/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
	at javafx.base@20/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
	at javafx.base@20/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
	at javafx.base@20/javafx.event.Event.fireEvent(Event.java:198)
	at javafx.graphics@20/javafx.scene.Scene$MouseHandler.process(Scene.java:3980)
	at javafx.graphics@20/javafx.scene.Scene.processMouseEvent(Scene.java:1890)
	at javafx.graphics@20/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2704)
	at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:411)
	at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:301)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
	at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:450)
	at javafx.graphics@20/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
	at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:449)
	at javafx.graphics@20/com.sun.glass.ui.View.handleMouseEvent(View.java:551)
	at javafx.graphics@20/com.sun.glass.ui.View.notifyMouse(View.java:937)
	at javafx.graphics@20/com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
	at javafx.graphics@20/com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:316)
	at java.base/java.lang.Thread.run(Thread.java:1623)

---------- BEGIN SOURCE ----------
package com.kwvanderlinde;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;

public class JavaFxNpe extends Application {
    private final EventListener listener = this::onButtonClicked;
    private WebView webView;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("NPE on WebView event");

        Pane root = new VBox();

        webView = new WebView();
        final var button = new Button("Open Page");

        button.setOnAction(event -> loadWebPage());
        root.getChildren().add(button);
        root.getChildren().add(webView);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();

        webView.getEngine().getLoadWorker().stateProperty().addListener(this::onStateChanged);
    }

    private void loadWebPage() {
        webView.getEngine().loadContent("""
                <!DOCTYPE html>
                <html lang="en">
                  <head>
                    <meta charset="utf-8">
                    <title>Submit Test</title>
                  </head>
                  <body>
                    <button>Close Page</button>
                  </body>
                </html>
                """);
    }

    private void onStateChanged(ObservableValue<? extends Worker.State> observable,
                                Worker.State oldState,
                                Worker.State newState) {
        if (newState != Worker.State.SUCCEEDED) {
            return;
        }

        final var nodeList = webView.getEngine().getDocument().getElementsByTagName("button");
        for (int i = 0; i < nodeList.getLength(); i++) {
            final var eventTarget = (EventTarget) nodeList.item(i);
            eventTarget.addEventListener("click", this.listener, true);
        }
    }

    private void onButtonClicked(Event event) {
        // On JFX thead
        webView.getEngine().load("about:blank");
        // Need to delay the GC to after this event is handled, but we also want a GC before we open
        // the page again.
        Platform.runLater(System::gc);
    }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Registering a different EventListener instance each time will avoid the issue. E.g., in the example code, I could use `this::onButtonClicked` instead of `this.listener` and everything will work fine.

FREQUENCY : always
Comments
Kindly find the analysis : !. The behavior is expected. 2. In the program, the web page is loaded and even registered by JAVA DOM API. 3. Click the "Open Page" button to open a web page in the WebView with a single button in it. It works, because, the dom element is live 4. Click the "Close Page" button in the WebView to close the page (load about blank) It will also work, but after loading about: blank, DOM elements ( like button) are not lived, it must be cleaned. when the dom element goes out of scope, it deregisters its event listener. the corresponding node in Java must be cleaned. 5. Click the "Open Page" button again to reopen the web page. Now, the HTML content is loaded, in this case, the dom node (Java side ) is a new instance, unaware of the old listener. 6. Click the "Close Page" button again. The issue occurs because a new dom node was created due to unloading ( by about: blank ) Note: We need to re-register the event listener with every unload, this is how we usually write JS HTML code. The test 8u341: Pass, because before 8u341, we were not cleaning the event listener properly and suffered a memory leak.
14-09-2023

Checked with attached testcase in Windows 10 and ubuntu 20.04, issue is reproducible in update releases of OCT 2022, Test Result ============ 8u341: Pass 8u351: Fail 8u381: Fail jfx11: Fail jfx17: Fail jfx20: Fail jfx21ea30: Fail Issue is not reproducible in update releases of CPU22_07 (previous update releases of CPU22_10)
08-09-2023

This regression was either caused by the update to WebKit 614.1 or, more likely, the fix for JDK-8088420 which was a fix for a memory leak in event listeners.
08-09-2023

We need to evaluate whether this is a valid use case, and whether we can fix it without reintroducing the memory leak described in JDK-8088420.
08-09-2023