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.)
Environment
Stack trace
Source citation
ComponentConnectionContext.java, lines 316–337 in CE 7.0.0:Why this fires
Classic time-of-check / time-of-use race. At line 317 the method validates
localUI != null. At line 321 it readslocalUI.getSession(), captures the result in a local. The lambda passed tobackgroundRunner.execute(...)runs on another thread later. By the time the lambda dereferencessession, the UI may have been detached and its session cleared. CE only guardslocalUI, notsession.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
flushPendingActionsIfActiveis invoked from a detach-adjacent path.Suggested fix
Treat
sessionas nullable inside the lambda, symmetric with the existinglocalUIcheck:Alternatively check inside the outer body before submitting (less robust against the race but cheaper):
Impact
The exception is thrown on a
pool-*-thread-Nbackground thread and propagates out ofThreadPoolExecutor.runWorkeras 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")):Driver (Playwright, two contexts on
?topic=<shared>): tab A fires the burst, tabB is abruptly closed via
context.close()mid-burst. The narrow TOCTOU window betweenlocalUI.getSession()(line 321) and the deferredsession.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.)