Repository navigation
Description
Five mutation hooks whose failures are already handled still throw them to the route error boundary, so a routine rejection replaces the whole page with the error page. The app's QueryClient sets throwOnError: true for mutations (src/services/react-query.ts), and useMutation rethrows the error during render whenever that is on, even if the caller awaited mutateAsync inside a try. The add-web-data-hook playbook (step 10) says an expected rejection needs throwOnError: false and meta.disableDefaultErrorNotification and an onError notification. useDeleteSeriesInstrumentMutation and useUpdateSeriesInstrumentArchiveMutation do all three. These five do not:
| Hook | Caller's intended failure behaviour | What happens instead |
|---|---|---|
useCreateInstrumentRepoMutation |
onError toast, Add Repository dialog stays open |
toast, then the Instrument Repos page is replaced |
useSyncInstrumentRepoMutation |
onError toast |
toast, then the page is replaced |
useDeleteInstrumentRepoMutation |
onError toast with the API's message (e.g. the 409 when a group was assigned the repo after the page loaded) |
toast, then the page is replaced |
useCreateSeriesInstrumentMutation |
manage.tsx catches the error and keeps the dialog open "for retry" |
Manage Group page is replaced, losing its unsaved instrument selection |
useUpdateUserMutation |
admin/users/$userId.tsx catches the error and shows "Could not save all changes…" inline (e.g. on the 403 when an admin disables their own account) |
generic "HTTP Request Failed" toast, then the page is replaced |
#1671 (useUpdateSetupStateMutation on the login page editor) and #1687 (useInstrumentBundle in the instrument preview dialog) report the same error-page replacement in other places. This issue covers only the five hooks above.
The unit tests miss this because they build a QueryClient without the app's throwOnError: true default, and the $userId route test mocks the hook outright. useDeleteSeriesInstrumentMutation.test.ts shows the pattern that would catch it.
Where
apps/web/src/hooks/useCreateInstrumentRepoMutation.ts:14-44apps/web/src/hooks/useSyncInstrumentRepoMutation.ts:13-53apps/web/src/hooks/useDeleteInstrumentRepoMutation.ts:13-36apps/web/src/hooks/useCreateSeriesInstrumentMutation.ts:16-44apps/web/src/hooks/useUpdateUserMutation.ts:12-24
For example, useDeleteInstrumentRepoMutation.ts:
return useMutation({
mutationFn: ({ id }: { id: string }) =>
axios.delete(`/v1/instrument-repos/${id}`, { meta: { disableDefaultErrorNotification: true } }),
onError(err) {
addNotification({ message: getApiErrorMessage(err, /* … */), type: 'error' });
},
// no throwOnError: falseReproduce
- As an admin, open Admin Panel → Instrument Repos → Add Repository.
- Enter the URL of a GitHub repository that does not exist (or a private one, without a token) and submit.
Actual: the "Failed to import repository" toast appears, and the page is replaced by the error page. Verified for useUpdateUserMutation by rendering it under the app's queryClient with a rejecting axios.patch: the error boundary rendered and the caller's catch message never did.
Expected: the toast appears and the Add Repository dialog stays open with the URL, ready to correct. Likewise, a failed series creation leaves the dialog open, and a failed user save shows the inline "Could not save all changes" alert.
Tests
For each hook, in apps/web/src/hooks/__tests__/<hook>.test.ts, render with new QueryClient({ defaultOptions: { mutations: { throwOnError: true } } }) as useDeleteSeriesInstrumentMutation.test.ts does, and add it('should report a rejection through its own notification without escaping to the route error boundary'). For useUpdateUserMutation, also assert the request sets disableDefaultErrorNotification, so the caller's inline message is the only one.
testing/src/specs/admin-instrument-repos.spec.ts: route POST **/v1/instrument-repos to a 400 with a message, submit the Add Repository form, and assert the toast is shown and the dialog is still open.
Suggested fix
Add throwOnError: false to all five hooks. In useUpdateUserMutation, also pass meta: { disableDefaultErrorNotification: true } on both requests, since its caller renders its own message.
Activity
fix(web): keep handled mutation failures off the route error boundary