Skip to content

Commit

Permalink
fix: wrap UI closing to catch exceptions (#20645)
Browse files Browse the repository at this point in the history
When session destroy is triggered,
wraps each UI close/detach to
catch exceptions, making sure
session destroy listeners are called.

Fixes #20293
  • Loading branch information
tepi authored Dec 9, 2024
1 parent e988d48 commit c194e71
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 12 deletions.
28 changes: 16 additions & 12 deletions flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java
Original file line number Diff line number Diff line change
Expand Up @@ -702,18 +702,22 @@ public void fireSessionDestroy(VaadinSession vaadinSession) {
}
List<UI> uis = new ArrayList<>(session.getUIs());
for (final UI ui : uis) {
ui.accessSynchronously(() -> {
/*
* close() called here for consistency so that it is always
* called before a UI is removed. UI.isClosing() is thus
* always true in UI.detach() and associated detach
* listeners.
*/
if (!ui.isClosing()) {
ui.close();
}
session.removeUI(ui);
});
try {
ui.accessSynchronously(() -> {
/*
* close() called here for consistency so that it is
* always called before a UI is removed. UI.isClosing()
* is thus always true in UI.detach() and associated
* detach listeners.
*/
if (!ui.isClosing()) {
ui.close();
}
session.removeUI(ui);
});
} catch (Exception e) {
session.getErrorHandler().error(new ErrorEvent(e));
}
}
SessionDestroyEvent event = new SessionDestroyEvent(
VaadinService.this, session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.vaadin.flow.server;

import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.i18n.DefaultI18NProvider;
import com.vaadin.flow.i18n.I18NProvider;
import net.jcip.annotations.NotThreadSafe;
Expand All @@ -24,12 +26,14 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -353,6 +357,64 @@ public void testSessionDestroyListenerCalled_whenAnotherListenerThrows()
listener.callCount);
}

@Test
public void testSessionDestroyListenerCalled_andOtherUiDetachCalled_whenUiClosingThrows()
throws ServiceException {
VaadinService service = createService();

TestSessionDestroyListener listener = new TestSessionDestroyListener();

service.addSessionDestroyListener(listener);

final AtomicBoolean secondUiDetached = new AtomicBoolean();
List<UI> UIs = new ArrayList<>();
MockVaadinSession vaadinSession = new MockVaadinSession(service) {
@Override
public Collection<UI> getUIs() {
return UIs;
}
};
vaadinSession.lock();
UI throwingUI = new UI() {
@Override
protected void onDetach(DetachEvent detachEvent) {
throw new RuntimeException();
}

@Override
public VaadinSession getSession() {
return vaadinSession;
}
};

throwingUI.getInternals().setSession(vaadinSession);
UIs.add(throwingUI);

UI detachingUI = new UI() {
@Override
protected void onDetach(DetachEvent detachEvent) {
secondUiDetached.set(true);
}

@Override
public VaadinSession getSession() {
return vaadinSession;
}
};
detachingUI.getInternals().setSession(vaadinSession);
UIs.add(detachingUI);

vaadinSession.unlock();

service.fireSessionDestroy(vaadinSession);

Assert.assertTrue("Second UI detach not called properly",
secondUiDetached.get());

Assert.assertEquals("SessionDestroyListener not called exactly once", 1,
listener.callCount);
}

@Test
public void testServiceDestroyListenerCalled_whenAnotherListenerThrows()
throws ServletException, ServiceException {
Expand Down

0 comments on commit c194e71

Please # to comment.