Skip to content

Session expires after one hour without warning, and in-progress instrument data is lost #1535

Description

@gdevenyi

What Happened?

The API issues access tokens that expire after one hour (auth.service.ts#L75). There is no refresh endpoint. apps/web does not read the token's exp claim (auth.slice.ts#L9) and has no 401 handler. The token is kept only in memory, so a page reload goes to the login page.

After the token expires, a user who is filling in an instrument can lose the data in two ways.

1. The user comes back to the tab. This is the more severe case. The user does nothing to cause it.

  1. TanStack Query refetches the instrument bundle when the tab becomes visible again, because useInstrumentBundle uses the default staleTime: 0 (useInstrumentBundle.ts#L7-L19).
  2. The API returns 401.
  3. Queries set throwOnError: true (react-query.ts#L12-L13). TanStack Query v5 sends a failed background refetch to the error boundary, even when the query already has data.
  4. AppErrorComponent replaces the render page with ErrorPage (AppErrorComponent.tsx#L36-L38). It shows "401 - Unauthorized" and tells the user to send an error report to the platform administrator (ErrorPage.tsx#L21-L35).
  5. The form unmounts, and the entered data is lost. The only way back is "Reload Page" and a new login, which also clears currentSession.

2. The user submits the instrument. The submit handler is a raw axios.post (render/$id.tsx#L41), and FormContent calls it with void (FormContent.tsx#L60). The 401 becomes an unhandled promise rejection. It does not go to the error boundary. The user sees only the generic "HTTP Request Failed / 401" toast (axios.ts#L200-L207). The result for the entered data depends on the instrument kind:

Kind Result
FORM The values stay on the screen. The user cannot sign in again without a page reload, so the values are lost.
FORM with resetButton The values are cleared at once. The libui Form resets after onSubmit returns, and the voided promise returns at once.
INTERACTIVE The task has ended, and there is no way to submit its result again. The result is lost.
FILE The upload status is FAILED. The selected files stay.

What Did You Expect?

The app should warn the user before the session expires. It should let the user sign in again without losing the instrument that is open. At minimum, an expired session should send the user to the login page with a clear "session expired" message.

Operating System

Any

Browser (if applicable)

Chrome, Firefox, Safari, Edge

Steps to Reproduce

Use pnpm dev:test, not pnpm dev. When NODE_ENV is development, the API ignores token expiry (jwt.strategy.ts#L18).

  1. Log in and open an instrument for a subject. Enter some data.
  2. Wait more than one hour, or sign a short-lived token for testing.
  3. Switch to another tab, then come back. The error page replaces the form.

For the second case, do steps 1 and 2, then submit the instrument. A 401 toast shows, and the record is not saved.

Anything Else?

Possible approaches, from smallest to largest:

  1. Add a 401 response interceptor in services/axios.ts that goes to /auth/login with a "session expired" message.
    • Skip requests that set meta.disableDefaultAuth, for example the login request.
    • Do not import the router in axios.ts, because that makes a circular import. Use window.location.assign.
    • The login page has no "session expired" message now. Add one.
    • This approach still loses the data that is on the screen.
  2. Decode exp in the auth slice. Warn the user a few minutes before expiry and offer re-authentication in a dialog, so the current route and form state stay mounted.
    • jwt-decode is already a dependency of apps/web, and apps/playground already checks exp (playground auth.slice.ts#L17).
    • The login request must move out of routes/auth/login.tsx into a shared function, so that the dialog can call it.
  3. Add a refresh-token flow. This conflicts with the memory-only token design, so it needs a decision from the maintainers.

Approaches 2 and 3 must keep in-progress instrument data across re-authentication.

Separately, a failed background refetch should not unmount a page that already has data. One option is throwOnError: (error, query) => query.state.data === undefined in services/react-query.ts. apps/web/AGENTS.md makes throwOnError: true a rule, so this change also needs a decision from the maintainers. See also #1538.

Tests. A unit test for the chosen expiry handling. An e2e test that uses a forged expired token, or page.route to return 401, for both cases: a refetch when the tab becomes visible on the render page, and a submit. Do not wait one hour in a test.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions