Skip to content

Session expiry without losing work

Status: ✅ Implemented

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.

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 visibilitychange refresh 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 onTokenExpired and 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.

  • Idle-session warning — the client-side counterpart: warn before anything expires
  • Autosave — shrink the window of unsaved work
  • Undo — recover from the opposite problem