authenticationrxjsrefresh-tokenredux-observableepic

How is this redux observable Epic works?


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:

  1. 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.
  2. 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.


Solution

    1. 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.

    It looks like the main problem is hidden in the source observable. ofType should 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.

    1. Why does mergeMap() have no effect here?

    The line mergeMap((userId) => RequestGetActiveWorkSession(userId).pipe( could be problematic. The source observable RequestGetActiveWorkSession(userId) apparently does not support re-subscription which is done on error.

    Please wrap the source observable to Observable.defer as it is mentioned in referenced guides.

    Cite guide #1:

    return Observable.defer(() => {
      // we have to use defer to create a block that only gets executed when source get resubscribed to it.
      return Observable.ajax(getData(data.id)) // api call to fetch data
    })
    

    Addendum to Question 1:

    Your custom action creator refreshSuccessActionCreator is not properly working in ofType operator. That's because the method refreshSuccessActionCreator.toString returns unexpected value.

    In order to pass the action creator to the ofType operator, 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.