diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java
index 19f3f5328f9..1b7b4a83d69 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java
@@ -175,16 +175,25 @@ public void valueBound(HttpSessionBindingEvent arg0) {
*/
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
- // If we are going to be unbound from the session, the session must be
- // closing
- // Notify the service
- if (service == null) {
+ // The Vaadin session instance may be not yet initialized properly via
+ // {@link #refreshTransients(WrappedSession, VaadinService)}. It happens
+ // when the session is deserialized and container decides that it's
+ // expired. Such session is not known to anyone (except deserializer)
+ // and nothins should be done with it: just return immediately.
+ //
+ // Be aware that not initialized session doesn't have the
+ // correct/expected state: it has no lock, service, session, etc.
+ if (!isInitialized()) {
getLogger().warn(
"A VaadinSession instance not associated to any service is getting unbound. "
+ "Session destroy events will not be fired and UIs in the session will not get detached. "
+ "This might happen if a session is deserialized but never used before it expires.");
- } else if (VaadinService.getCurrentRequest() != null
- && getCurrent() == this) {
+ return;
+ }
+ // If we are going to be unbound from the session, the session must be
+ // closing
+ // Notify the service
+ if (VaadinService.getCurrentRequest() != null && getCurrent() == this) {
checkHasLock();
// Ignore if the session is being moved to a different backing
// session or if GAEVaadinServlet is doing its normal cleanup.
@@ -632,8 +641,10 @@ public void removeUI(UI ui) {
* Lock
interface than {@link Lock#lock()} and
* {@link Lock#unlock()}.
*
- * @return the Lock
that is used for synchronization, never
- * null
+ * @return the Lock
that is used for synchronization, it's
+ * never null
for the session which is in use, i.e. if
+ * {@link #refreshTransients(WrappedSession, VaadinService)} has
+ * been called for it
* @see #lock()
* @see Lock
*/
@@ -1116,4 +1127,22 @@ public StreamResourceRegistry getResourceRegistry() {
public String getCsrfToken() {
return csrfToken;
}
+
+ /**
+ * Checks whether the session is properly initialized/in use.
+ *
+ * The session is initialized if + * {@link #refreshTransients(WrappedSession, VaadinService)} has been called + * for it. If the session is just created or deserialized but not yet in + * real use then it's not initialized and it's state is incomplete. + * + * @return whether the session is initialized + */ + private boolean isInitialized() { + boolean isInitialized = service != null; + assert isInitialized + || session == null : "The wrapped session must be null if the service is null (which happens after deserialization)"; + return isInitialized; + } + } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/VaadinSessionTest.java b/flow-server/src/test/java/com/vaadin/flow/server/VaadinSessionTest.java index a95af7bbcea..d248964c1cc 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/VaadinSessionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/VaadinSessionTest.java @@ -483,4 +483,24 @@ protected Lock getSessionLock(WrappedSession wrappedSession) { Assert.assertNull(vaadinSession.getSession()); } + @Test + public void valueUnbound_sessionIsNotInitialized_noAnyInteractions() { + VaadinSession session = Mockito.spy(TestVaadinSession.class); + + HttpSessionBindingEvent event = Mockito + .mock(HttpSessionBindingEvent.class); + session.valueUnbound(null); + + Mockito.verify(session).valueUnbound(null); + Mockito.verifyNoInteractions(event); + Mockito.verifyNoMoreInteractions(session); + } + + public static class TestVaadinSession extends VaadinSession { + + public TestVaadinSession() { + super(null); + } + } + }