🐛(frontend) redirection 401 overridden

To capture a 401 we were using "onError" in the
queryClient default mutation options. The problem
is this way does not capture globally the onError,
if a mutation uses as well is own "onError", it will
override the default one, causing the 401 to
not be captured anymore.
We now use MutationCache, which allows us to
capture globally the onError, even if a mutation
has its own "onError" defined, this global one will
still be called.
This commit is contained in:
Anthony LC
2025-07-24 11:45:09 +02:00
parent 0b301b95c8
commit 04273c3b3e
4 changed files with 45 additions and 28 deletions

View File

@@ -23,6 +23,7 @@ and this project adheres to
- 🐛(service-worker) Fix useOffline Maximum update depth exceeded #1196 - 🐛(service-worker) Fix useOffline Maximum update depth exceeded #1196
- 🐛(helm) charts generate invalid YAML for collaboration API / WS #890 - 🐛(helm) charts generate invalid YAML for collaboration API / WS #890
- 🐛(frontend) 401 redirection overridden #1214
## [3.4.2] - 2025-07-18 ## [3.4.2] - 2025-07-18

View File

@@ -9,6 +9,7 @@ import {
mockedDocument, mockedDocument,
verifyDocName, verifyDocName,
} from './utils-common'; } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Routing', () => { test.describe('Doc Routing', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@@ -60,16 +61,20 @@ test.describe('Doc Routing', () => {
}); });
test('checks 401 on docs/[id] page', async ({ page, browserName }) => { test('checks 401 on docs/[id] page', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, '401-doc', browserName, 1); const [docTitle] = await createDoc(page, '401-doc-parent', browserName, 1);
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await createRootSubPage(page, browserName, '401-doc-child');
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
const responsePromise = page.route( const responsePromise = page.route(
/.*\/link-configuration\/$|users\/me\/$/, /.*\/documents\/.*\/$|users\/me\/$/,
async (route) => { async (route) => {
const request = route.request(); const request = route.request();
if ( if (
request.method().includes('PUT') || request.method().includes('PATCH') ||
request.method().includes('GET') request.method().includes('GET')
) { ) {
await route.fulfill({ await route.fulfill({
@@ -84,11 +89,7 @@ test.describe('Doc Routing', () => {
}, },
); );
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('link', { name: '401-doc-parent' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page.getByLabel('Connected').click();
await responsePromise; await responsePromise;

View File

@@ -1,5 +1,9 @@
import { CunninghamProvider } from '@openfun/cunningham-react'; import { CunninghamProvider } from '@openfun/cunningham-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import {
MutationCache,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -24,8 +28,24 @@ const defaultOptions = {
retry: DEFAULT_QUERY_RETRY, retry: DEFAULT_QUERY_RETRY,
}, },
}; };
let globalRouterReplace: ((url: string) => void) | null = null;
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions, defaultOptions,
mutationCache: new MutationCache({
onError: (error) => {
if (error instanceof Error && 'status' in error && error.status === 401) {
void queryClient.resetQueries({
queryKey: [KEY_AUTH],
});
setAuthUrl();
if (globalRouterReplace) {
void globalRouterReplace('/401');
}
}
},
}),
}); });
export function AppProvider({ children }: { children: React.ReactNode }) { export function AppProvider({ children }: { children: React.ReactNode }) {
@@ -40,25 +60,14 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
return initializeResizeListener(); return initializeResizeListener();
}, [initializeResizeListener]); }, [initializeResizeListener]);
/**
* Update the global router replace function
* This allows us to use the router replace function globally
*/
useEffect(() => { useEffect(() => {
queryClient.setDefaultOptions({ globalRouterReplace = (url: string) => {
...defaultOptions, void replace(url);
mutations: { };
onError: (error) => {
if (
error instanceof Error &&
'status' in error &&
error.status === 401
) {
void queryClient.resetQueries({
queryKey: [KEY_AUTH],
});
setAuthUrl();
void replace(`/401`);
}
},
},
});
}, [replace]); }, [replace]);
return ( return (

View File

@@ -52,10 +52,16 @@ export function useUpdateDoc(queryConfig?: UseUpdateDoc) {
void queryConfig.onSuccess(data, variables, context); void queryConfig.onSuccess(data, variables, context);
} }
}, },
onError: () => { onError: (error, variables, context) => {
// If error it means the user is probably not allowed to edit the doc
// so we invalidate the canEdit query to update the UI accordingly
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
queryKey: [KEY_CAN_EDIT], queryKey: [KEY_CAN_EDIT],
}); });
if (queryConfig?.onError) {
queryConfig.onError(error, variables, context);
}
}, },
}); });
} }