Skip to content

NullPointerException in ComponentConnectionContext.flushPendingActionsIfActive when VaadinSession becomes null between probe and deferred execution (CE 7.0.0) #139

@enver-haase

Description

@enver-haase

Environment

  • Vaadin Flow 25.1.x
  • Vaadin Collaboration Engine 7.0.0
  • Spring Boot 4.0.x, Java 25
  • Two concurrent browser sessions exercising the same collaboration topic, with one detaching while pending actions are in flight.

Stack trace

Exception in thread "pool-3-thread-2" java.lang.NullPointerException:
  Cannot invoke "com.vaadin.flow.server.VaadinSession.access(com.vaadin.flow.server.Command)"
  because "session" is null
    at com.vaadin.collaborationengine.ComponentConnectionContext
         .lambda$flushPendingActionsIfActive$1(ComponentConnectionContext.java:322)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
    at java.base/java.lang.Thread.run(Thread.java:1516)

Source citation

ComponentConnectionContext.java, lines 316–337 in CE 7.0.0:

private void flushPendingActionsIfActive() {
    UI localUI = this.ui;
    if (localUI == null || backgroundRunner == null) {
        return;
    }
    VaadinSession session = localUI.getSession();           // line 321: read; may return null
    backgroundRunner.execute(() -> session.access(() -> {  // line 322: deferred; session may be null by now
        UI currentUI = UI.getCurrent();
        ...
    }));
}

Why this fires

Classic time-of-check / time-of-use race. At line 317 the method validates localUI != null. At line 321 it reads localUI.getSession(), captures the result in a local. The lambda passed to backgroundRunner.execute(...) runs on another thread later. By the time the lambda dereferences session, the UI may have been detached and its session cleared. CE only guards localUI, not session.

Reproduces reliably in two-user automated test scenarios where one user navigates away or closes the browser tab while the other user is producing edits that fan out via the shared topic — i.e. anywhere flushPendingActionsIfActive is invoked from a detach-adjacent path.

Suggested fix

Treat session as nullable inside the lambda, symmetric with the existing localUI check:

backgroundRunner.execute(() -> {
    if (session == null) return; // UI detached after we captured the session
    session.access(() -> { ... });
});

Alternatively check inside the outer body before submitting (less robust against the race but cheaper):

if (session == null) return;

Impact

The exception is thrown on a pool-*-thread-N background thread and propagates out of ThreadPoolExecutor.runWorker as an uncaught exception. Downstream effect in observed reports: server-side application code expecting CE-driven broadcasts to reach the remaining collaborator(s) sees inconsistent state — the collaborator's UI never receives the value the typing user just produced — making any automated check that asserts cross-session propagation flaky (~25% failure rate in tight teardown sequences).

Reproducer

A self-contained Vaadin 25.1.1 / Spring Boot 4.0.5 / CE 7.0.0 reproducer is available
that fires this NPE reliably (two firings observed on the first run in a verified
session 2026-05-13).

Key view (SessionNpeView.java, @Route("session-npe")):

TextField name = new TextField("Name");
name.setValueChangeMode(ValueChangeMode.EAGER);
CollaborationBinder<Person> binder = new CollaborationBinder<>(Person.class, user);
binder.bind(name, "name");
binder.setTopic(topic, Person::new);

Button burst = new Button("Burst-type 50 chars", _ev -> {
    for (int i = 0; i < 50; i++) {
        name.setValue("burst-" + System.nanoTime());
    }
});

Driver (Playwright, two contexts on ?topic=<shared>): tab A fires the burst, tab
B is abruptly closed via context.close() mid-burst. The narrow TOCTOU window between
localUI.getSession() (line 321) and the deferred session.access(...) lambda
(line 322) fires on tab A's pending-action flush.

(Full project / build instructions / driver spec available on request, or can be
inlined in this thread if useful.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions