Skip to content

Multi-user app context does not isolate Spring session-scoped beans per user #110

Description

@Artur-

Summary

When using the 1.1 multi-user API (SpringBrowserlessApplicationContext
newUser()newWindow()), each user gets its own VaadinSession, but
Spring-managed session-scoped beans (@SessionScope and, by extension,
@VaadinSessionScope) are not resolved per user. The second user reuses the
first user's session-scoped bean instance. When such a bean holds a local
ValueSignal (or ListSignal), the second user's UI then accesses a signal that
was created in the first user's VaadinSession, which fails with:

java.lang.IllegalStateException: This ValueSignal instance was created in one
VaadinSession but is being accessed from another. This typically happens when a
local signal is stored in a static field or an application-scoped bean. Use
SharedValueSignal or SharedListSignal instead for state that is shared across
sessions.
    at com.vaadin.flow.signals.local.AbstractLocalSignal.checkPreconditions(AbstractLocalSignal.java:71)

This blocks multi-user testing for any app whose views (or shared parent layout)
depend on a Spring session-scoped bean — a very common pattern.

Environment

  • browserless-test: 1.1.x (Vaadin platform 25.2)
  • Vaadin Flow: 25.2
  • Spring Boot: 4.1.0
  • Java: 25

Expected behavior

Each BrowserlessUserContext created via newUser(...) should resolve Spring
session-scoped beans (@SessionScope, @VaadinSessionScope) against that user's
own session, so two users get independent instances — mirroring two real browser
sessions. A signal stored in a @SessionScope bean should be per-user.

Actual behavior

The second user reuses the first user's session-scoped bean instance. The bean's
local ValueSignal belongs to the first user's VaadinSession, so binding it in
the second user's UI throws the cross-session IllegalStateException above and
navigation fails.

Key diagnostic

  • Single user, single (or multiple) window through the same app context: works.
    A @SessionScope bean with a local ValueSignal, bound in a view, renders
    fine for one user.
  • Two users: the second fails on the cross-session signal access.

This pins the gap to per-user session-scope isolation, not to the API surface,
signals support, or navigation in general.

Minimal reproduction

// A Spring session-scoped bean holding a per-session local signal.
@org.springframework.stereotype.Component
@org.springframework.web.context.annotation.SessionScope
class Prefs {
    final com.vaadin.flow.signals.local.ValueSignal<String> color =
            new com.vaadin.flow.signals.local.ValueSignal<>("white");
}

@Route("repro")
class ReproView extends com.vaadin.flow.component.html.Div {
    ReproView(Prefs prefs) {
        // Binding the session-scoped signal is what trips the cross-session check
        getStyle().bind("background-color", prefs.color);
    }
}

@SpringBootTest
class MultiUserReproTest {
    @Autowired ApplicationContext applicationContext;

    @Test
    void twoUsersEachGetOwnSessionScopedBean() {
        try (var app = SpringBrowserlessApplicationContext
                .createSecured(applicationContext, ReproView.class);
             var alice = app.newUser("alice", "USER");
             var bob   = app.newUser("bob", "USER")) {

            alice.newWindow().navigate(ReproView.class); // OK
            bob.newWindow().navigate(ReproView.class);   // throws: ValueSignal
                                                          // created in another session
        }
    }
}

(Replace Prefs with any real @SessionScope / @VaadinSessionScope bean that
exposes a local ValueSignal or ListSignal.)

Likely cause

Each BrowserlessUserContext creates its own session objects, but Spring's
session-scope resolution (RequestContextHolder / the scoped bean store) does not
appear to be (re)bound to the active user's session when
BrowserlessUIContext.activate() installs that user's thread-locals. So the
first resolution of a session-scoped bean is cached and handed to every user.

Suggested direction

On user/window activation, bind the Spring request/session context
(RequestContextHolder, and the @VaadinSessionScope store) to the active
BrowserlessUserContext's session so that session-scoped beans resolve per user.
A regression test asserting two users receive distinct @SessionScope and
@VaadinSessionScope bean instances (and distinct local signals) would lock it
in.

Impact

High for collaborative apps: the whole point of the multi-user API is testing
cross-session collaboration, and those apps almost always keep per-session UI
state (current user, preferences, presence) in session-scoped signal beans —
exactly the beans that currently leak across users.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    Status
    🔖 High Priority (P1)
    Status
    🔎Iteration reviews

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions