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.
- TanStack Query refetches the instrument bundle when the tab becomes visible again, because
useInstrumentBundle uses the default staleTime: 0 (useInstrumentBundle.ts#L7-L19).
- The API returns 401.
- 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.
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).
- 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).
- Log in and open an instrument for a subject. Enter some data.
- Wait more than one hour, or sign a short-lived token for testing.
- 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:
- 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.
- 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.
- 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.
What Happened?
The API issues access tokens that expire after one hour (auth.service.ts#L75). There is no refresh endpoint.
apps/webdoes not read the token'sexpclaim (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.
useInstrumentBundleuses the defaultstaleTime: 0(useInstrumentBundle.ts#L7-L19).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.AppErrorComponentreplaces the render page withErrorPage(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).currentSession.2. The user submits the instrument. The submit handler is a raw
axios.post(render/$id.tsx#L41), andFormContentcalls it withvoid(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:resetButtonFormresets afteronSubmitreturns, and the voided promise returns at once.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, notpnpm dev. WhenNODE_ENVisdevelopment, the API ignores token expiry (jwt.strategy.ts#L18).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:
services/axios.tsthat goes to/auth/loginwith a "session expired" message.meta.disableDefaultAuth, for example the login request.axios.ts, because that makes a circular import. Usewindow.location.assign.expin 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-decodeis already a dependency ofapps/web, andapps/playgroundalready checksexp(playground auth.slice.ts#L17).routes/auth/login.tsxinto a shared function, so that the dialog can call it.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 === undefinedinservices/react-query.ts.apps/web/AGENTS.mdmakesthrowOnError: truea 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.routeto 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.