Now I'm working on website authentication, and I have added access and refresh tokens. I want to create an auto-refresh action to obtain a new access token. So, if my server returns an "access denied" error, I want to perform a refresh and retry the previous action using RxJS and Redux Observable.
I success to make refresh request when i got this error (i was following these guides #1, #2). However, I'm still unable to figure out how to call the main action again. I want the actions to be executed in the order of main action, refresh, main action (again), but currently, only two out of the three actions are being executed.
Here is my code, so handleError function is called in catchError to request new access token if needed
const handleError = (action$: Observable<any>, err: any, caught: Observable<any>) => {
if (err.response?.errors?.[0]?.extensions?.code === "ACCESS_DENIED") {
return action$.pipe(
ofType(refreshSuccessActionCreator),
takeUntil(action$.pipe(ofType(LOGOUT_ACTION))),
take(1),
mergeMap(() => caught), //it is supposed to try main action again, but it doesn't
mergeWith(of(refreshActionCreator())), //calling refresh
);
}
else {
return of(workSessionErrorActionCreator(err));
}
};
//my epic
export const GetActiveWorkSessionEpic: Epic = (action$: Observable<PayloadAction<string>>) =>
action$.pipe(
ofType(GET_ACTIVE_WORK_SESSION_ACTION),
map(action => action.payload),
mergeMap((userId) => RequestGetActiveWorkSession(userId).pipe(
map(res => res.data?.workSession.getActiveWorkSessionByUserId),
map(workSession => SetActiveWorkSession(workSession)),
catchError((err, caught) => handleError(action$, err, caught)),
))
);
//action creators
export const refreshActionCreator = () => ({type: REFRESH_ACTION});
export const refreshSuccessActionCreator = () => ({type: REFRESH_SUCCESS_ACTION});
//refresh epic
export const RefreshEpic: Epic = (action$) =>
action$.pipe(
ofType(REFRESH_ACTION),
mergeMap(() => RequestRefresh().pipe(
map(res => {
if (res.data.auth.refresh) {
SetAccessToken(res.data.auth.refresh);
}
return refreshSuccessActionCreator();
}),
catchError(() => of(logoutActionCreator()))
))
);
Also, I don't understand how my handleError epics work:
- Why do I have to pass the actionCreator instead of the action type in
ofType()? When i put action type here it just don't work and infinitely request main action. - Why does
mergeMap()have no effect here?"
I was trying to change mergeMap and mergeWith to concatMap and concatWith. Also tryied some more rxjs operators here, but it don't work. Tryied to change placement in pipe(). I can`t to much as i don't understand this epic, but I have already spent many hours with it.
It looks like the main problem is hidden in the source observable.
ofTypeshould work as you expect. But the source observable probably only repeats first failure which would lead to infinite loop. To fix that see the answer of the 2nd question.The line
mergeMap((userId) => RequestGetActiveWorkSession(userId).pipe(could be problematic. The source observableRequestGetActiveWorkSession(userId)apparently does not support re-subscription which is done on error.Please wrap the source observable to
Observable.deferas it is mentioned in referenced guides.Cite guide #1:
Addendum to Question 1:
Your custom action creator
refreshSuccessActionCreatoris not properly working inofTypeoperator. That's because the methodrefreshSuccessActionCreator.toStringreturns unexpected value.In order to pass the action creator to the
ofTypeoperator, the action creator has to be created by createAction from redux-toolkit. Implementation details can be found in createAction.ts#L280 and operators.ts#L7.