enhancement
major
minor
major
minor
major
#29964
React UI: password login sometimes logs out immediately - a command of the replaced window triggers the stale-UI reload of the old window URL
Problem
After logging in with user name and password in the React UI (login dialog), the user is sometimes logged out again immediately and sees the anonymous application. Login via OpenID (Keycloak) is not affected. Observed on the test deployment of PR !1975; not reproducible there in a fresh Chrome at 1600×900, so it depends on timing.
The server log at that moment:
Command 'reportDisplayClass' for control 'v31' targets unknown window 'vaf8e206d7705d5c6'. Known windows: [va47f1cbd77dc0eee] S(…)
Cause
- A password login replaces the session: LoginAction.completeLogin records the login (PendingSessionAction.requestLogin) and sent the raw script window.location.reload(); to the page. The reload request swaps the session (PendingSessionAction.apply).
- While that navigation is in flight, the old page is still alive and keeps talking to the server: TLAppShell.tsx sends reportDisplayClass on mount and whenever the compact/regular breakpoint is crossed, the event stream reconnects.
- Such a request reaches the server after the swap. ReactServlet answers it with the error code stale-ui, the event stream reports a lost session.
- command-channel.ts and sse-client.ts then reloaded the page on their own - although the page was already leaving. That second navigation cancels the login navigation; a request still carrying the old session cookie is answered by ViewServlet.handleNoSession with a new anonymous session: logged out.
OpenID is not affected because its login ends in a full redirect to a new URL, without a reload inside a running page.
Two further defects of the same mechanism were found while verifying the fix:
- Language switch (also login/logout) in a polling tab reloaded twice. The window's event queue keeps what it wrote until a poll acknowledges it. The old page read the reload in its last poll and never acknowledged it, and the next page's first full poll received it again.
- Events of a newly rendered page were lost. While the next page of a window was rendered, the server still held the event stream of the previous page and wrote the new page's events (e.g. the page title) into it. The new page never got them (title stayed "TopLogic React Demo"), and the client's delivery check then switched the tab to polling.
Lösung
- The page knows that it is leaving (page-leave.ts in the React bridge). leavePage(url?) marks the page as leaving, closes the event stream and only then navigates (reload, or another address). While leaving, the command channel sends no further commands, and neither a stale-ui answer nor a lost event stream triggers another reload. pagehide sets the same state. For a navigation the browser starts itself (F5, a link), beforeunload opens a short grace period in which a closed event stream is taken for the navigation's side effect, not for a refused stream; a page that stays (cancelled prompt) reopens its stream afterwards.
- Server-triggered navigation is a typed event. Instead of evaluating JavaScript, the server sends PageNavigationEvent (sse.proto, field url, empty = reload), which the client executes through leavePage. Used by the login (LoginAction), the logout (LogoutCommand), the language switch (LanguageCommands) and the forced reload of obsolete windows (ReactWindowRegistry). A button's navigateUrl (SSO redirects) also leaves the page through leavePage.
- A stale-UI reload re-enters through the window-less URL (bootstrap page) and not through a URL that still names a discarded window. Command channel, event stream and the chat (tl-chat stale-window.ts) share one guarded reload (reloadForStaleUi, exported by tl-react-bridge).
- Every page render starts a new page in the window's event queue (SSEUpdateQueue.beginPage(), called by ViewServlet before the page is rendered): the previous page's stream and waiting poll are closed, what was written for it is forgotten, and the events of the page being rendered wait for its client and reach it numbered.
Verification: password login with an artificially delayed session swap (4 s) while the browser window crosses the compact breakpoint keeps the user logged in (3/3), the old page sends no command during the delay; logout, a stale second tab, F5, and the language switch in a streaming and in a polling tab each reload exactly once and keep the event delivery working. Tests: page-leave.test.ts, sse-client.test.ts, stale-window.test.ts, TestNewPageDelivery.