Session expiry without losing work
Status: ✅ Implemented
Intent
Section titled “Intent”The user spends twenty minutes on a form; the token expires; Save returns 401. The naive handling — redirect to login — destroys the work. The right contract: pause the action, re-authenticate, retry it; the page never navigates, the state never unloads.
Solution
Section titled “Solution”When any Mateu request returns 401, the api client does not fail the action. It hands control to the app’s re-auth flow and retries the same request once afterwards — the original action then completes as if nothing happened.
Plug your re-auth flow (programmatically, from your app’s bootstrap script):
import { onSessionExpired } from 'mateu';
onSessionExpired(async ({ retry, giveUp }) => { const ok = await openLoginPopup(); // your flow: popup, refresh token, SSO iframe… if (ok) { localStorage.setItem('__mateu_auth_token', ok.token); // where Mateu reads the Bearer retry(); // re-runs the failed request, action completes } else { giveUp(); // fail normally (user declined) }});Or listen to the DOM event (same contract, no imports):
document.addEventListener('mateu-session-expired', e => { e.preventDefault(); // claim it const { retry, giveUp } = e.detail; // …re-authenticate, then retry() or giveUp()});- The retry happens once per request (no loops when the new token is also rejected).
- Both web renderers raise it: the Vaadin one from its axios client, the Redwood one from its bridge’s single request path.
- In the Redwood renderer, 401s that arrive together share one event: a tab back from the
background whose periodic calls (inbox badge, banner sync) and the user’s action all bounce at
once triggers a single refresh, and all of them are retried with its token. A request whose
token was already replaced while it was in flight (the
visibilitychangerefresh landed first) is simply resent, without raising the event. - A 403 is not a session problem and never raises the event: the server knows who you are and refuses this (an undeclared action, a missing role). Redwood shows “No tienes permiso para hacer esto.” instead of the session message.
- With no handler registered, behavior is exactly as before: the request fails — the guard is fully opt-in.
- Because the page never navigates, everything holds: form values, wizard position, scroll, selection. Combine with the dirty guard and autosave for defense in depth.
Keycloak (the generated bootstrap page): handled for you
Section titled “Keycloak (the generated bootstrap page): handled for you”An app secured with Keycloak through Mateu’s generated bootstrap page needs none of the above — the page does it:
- it claims
mateu-session-expired, forces a token refresh (updateToken(-1)) and retries the request; if the refresh fails the Keycloak session itself is gone (expired, revoked, the realm was reset), so it sends the browser to the login instead of leaving a “session is no longer valid” message whose retry resends the same dead token; - it refreshes on
onTokenExpiredand again whenever the tab becomes visible — browsers throttle timers in background tabs and sleeping laptops, which is how a token expires unnoticed.
Container-managed login (basic auth / form login): the invisible 302
Section titled “Container-managed login (basic auth / form login): the invisible 302”Behind container-managed auth there is no 401: when the server session dies (e.g. the server restarted), API calls answer a 302 to the login page — and the browser follows it transparently, so the client sees a 200 whose body is the login page’s HTML instead of JSON. The user would just see the action silently do nothing.
Mateu detects this shape on every response — the request was redirected (the final URL differs from the requested one) and the payload is not JSON — and sends the browser to that final URL so the user can log in again. A redirect that still answers JSON (e.g. a trailing-slash rewrite) is left alone. This is built into the shared api client; nothing to configure.
One limit: if the login page lives on another origin without CORS, the browser hides the final URL from the client — that case surfaces as a plain network error instead.
Related
Section titled “Related”- Idle-session warning — the client-side counterpart: warn before anything expires
- Autosave — shrink the window of unsaved work
- Undo — recover from the opposite problem