1
votes

I am trying to setup ngrx effects for my angular application. I have been successful in developing the effects and actions which are working fine.

I am facing one issue which is when my service returns an unsuccessful status like 400,422 etc. The error is caught by the effect and appropriate action for failure is triggered but the payload I am receiving is unknown.

The service is giving out a proper JSON formatted body incase of error structured as follows:

{
    "data": null,
    "error": true,
    "errorBody": {
        "code": 422,
        "body": "Unprocessable Entity"
    }
}

with HTTP status code of 422.

This is my service code method which is using AngularFire Functions(123 is deliberately added so that service throws error):

getBySummonerAlias(
    summonerName: string,
    summonerTag: number
  ): Observable<Summoner> {
    const callable = this.fns.httpsCallable('getSummonerByAlias');
    return callable({ 123: summonerName, summonerTag: summonerTag });
  }

My actions:

export const loadSummoner = createAction('[Summoner] Load Summoner');

export const loadSummonerSuccess = createAction(
  '[Summoner] Load Summoner Success',
  props<{ summoner: Summoner }>()
);

export const loadSummonerFailure = createAction(
  '[Summoner] Load Summoner Failure',
  props<{ error: any }>()
);

My effects:

loadSummoner$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromSummonerActions.loadSummoner),
      switchMap(() =>
        this.summonerService.getBySummonerAlias('HawKEyE', 2311).pipe(
          map((summoner: Summoner) =>
            fromSummonerActions.loadSummonerSuccess({ summoner })
          ),
          catchError((error) =>
            of(fromSummonerActions.loadSummonerFailure({ error }))
          )
        )
      )
    )
  );

My reducer:

export const reducers = createReducer(
  fromSummonerStore.initialState,
  on(fromSummonerActions.loadSummonerSuccess, (state, action) => {
    return {
      summoner: action.summoner,
    };
  }),
  on(fromSummonerActions.loadSummonerFailure, (state, action) => {
    return {
      summoner: state.summoner,
      error: action.error,
    };
  })
);

export const metaReducers: MetaReducer<
  fromSummonerStore.SummonerState
>[] = !environment.production ? [] : [];

This is the Redux state:

This is the Redux state:

4
Can you please add the reducer also, just need to check how 'loadSummonerFailure' action is handled?rrr
@RenjithP.N.: I haved added the reducer in post. Thanks for looking into this :)Abhigyan Singh
Anything interesting in the console?, when you said the service is giving proper json format response, you checked that in browser network tab or postman?rrr

4 Answers

1
votes

The problem could be related with the structure required for your error in order to be processed by the AngularFire and Firebase Functions libraries.

As you can see in the source code, AngularFire is just wrapping the Firebase Functions API:

      const functions = of(undefined).pipe(
      observeOn(schedulers.outsideAngular),
      switchMap(() => import('firebase/functions')),
      tap((it: any) => it),
      map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
      map(app => app.functions(region || undefined)),
      tap(functions => {
        if (origin) {
          functions.useFunctionsEmulator(origin);
        }
      }),
      shareReplay({ bufferSize: 1, refCount: false })
    );

    this.httpsCallable = <T = any, R = any>(name: string) =>
      (data: T) => from(functions).pipe(
        observeOn(schedulers.insideAngular),
        switchMap(functions => functions.httpsCallable(name)(data)),
        map(r => r.data as R)
      );

In service.ts you can find the definition of the Firebase's httpCallable function. The important part is the call function invocation. In this function, you can find this code:

    // Check for an error status, regardless of http status.
    const error = _errorForResponse(
      response.status,
      response.json,
      this.serializer
    );
    if (error) {
      throw error;
    }

_errorForResponse is defined here, and looks like this:

/**
 * Takes an HTTP response and returns the corresponding Error, if any.
 */
export function _errorForResponse(
  status: number,
  bodyJSON: HttpResponseBody | null,
  serializer: Serializer
): Error | null {
  let code = codeForHTTPStatus(status);

  // Start with reasonable defaults from the status code.
  let description: string = code;

  let details: unknown = undefined;

  // Then look through the body for explicit details.
  try {
    const errorJSON = bodyJSON && bodyJSON.error;
    if (errorJSON) {
      const status = errorJSON.status;
      if (typeof status === 'string') {
        if (!errorCodeMap[status]) {
          // They must've included an unknown error code in the body.
          return new HttpsErrorImpl('internal', 'internal');
        }
        code = errorCodeMap[status];

        // TODO(klimt): Add better default descriptions for error enums.
        // The default description needs to be updated for the new code.
        description = status;
      }

      const message = errorJSON.message;
      if (typeof message === 'string') {
        description = message;
      }

      details = errorJSON.details;
      if (details !== undefined) {
        details = serializer.decode(details as {} | null);
      }
    }
  } catch (e) {
    // If we couldn't parse explicit error data, that's fine.
  }

  if (code === 'ok') {
    // Technically, there's an edge case where a developer could explicitly
    // return an error code of OK, and we will treat it as success, but that
    // seems reasonable.
    return null;
  }

  return new HttpsErrorImpl(code, description, details);
}

Your custom error structure could not be processed by this function, and the processing error is silently ignored in the catch block.

In this situation only the code is initialized by the following function:

/**
 * Takes an HTTP status code and returns the corresponding ErrorCode.
 * This is the standard HTTP status code -> error mapping defined in:
 * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
 *
 * @param status An HTTP status code.
 * @return The corresponding ErrorCode, or ErrorCode.UNKNOWN if none.
 */
function codeForHTTPStatus(status: number): FunctionsErrorCode {
  // Make sure any successful status is OK.
  if (status >= 200 && status < 300) {
    return 'ok';
  }
  switch (status) {
    case 0:
      // This can happen if the server returns 500.
      return 'internal';
    case 400:
      return 'invalid-argument';
    case 401:
      return 'unauthenticated';
    case 403:
      return 'permission-denied';
    case 404:
      return 'not-found';
    case 409:
      return 'aborted';
    case 429:
      return 'resource-exhausted';
    case 499:
      return 'cancelled';
    case 500:
      return 'internal';
    case 501:
      return 'unimplemented';
    case 503:
      return 'unavailable';
    case 504:
      return 'deadline-exceeded';
    default: // ignore
  }
  return 'unknown';
}

This is why you are receiving the value not-found as the error message.

Please, define a canonical Firebase error structure, I think it should work.

0
votes

It looks like of(fromSummonerActions.loadSummonerFailure({ error }) is causing the issue. Your error response is like

{
    "data": null,
    "error": true,
    "errorBody": {
        "code": 422,
        "body": "Unprocessable Entity"
    }
}

Because of destructuring({ error }), I think only the true value will go to your reducer.

0
votes

One thing you can do is log the information received by your Effect, something like the following:

loadSummoner$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromSummonerActions.loadSummoner),
      switchMap(() =>
        this.summonerService.getBySummonerAlias('HawKEyE', 2311).pipe(
          map((summoner: Summoner) =>
            fromSummonerActions.loadSummonerSuccess({ summoner })
          ),
          catchError((error) => {
            console.log('Received error from AngularFire:', error);
            return of(fromSummonerActions.loadSummonerFailure({ error }));
          })
        )
      )
    )
  );

This will give you insights about the problem (is the method actually being executed, which value has the error variable, etc).

You can also trace your service. I have not used AngularFire before but I think the problem is there as the ngrx code looks fine.

For instance, you can implement something like that:

  getBySummonerAlias(
    summonerName: string,
    summonerTag: number
  ): Observable<Summoner> {
    const callable = this.fns.httpsCallable('getSummonerByAlias');
    return callable({ 123: summonerName, summonerTag: summonerTag })
           .pipe(
              catchError((error: any) => {
                  console.log(error);
                  return throwError(error);
              })
           );
  }

This way you can be sure about if an error is actually raised by the function invocation and the value of the error returned.

0
votes

I dug deep in the AngularFire library and it is mapping the error codes to error strings internally in firebase/functions.js

function codeForHTTPStatus(status) {
// Make sure any successful status is OK.
if (status >= 200 && status < 300) {
    return 'ok';
}
switch (status) {
    case 0:
        // This can happen if the server returns 500.
        return 'internal';
    case 400:
        return 'invalid-argument';
    case 401:
        return 'unauthenticated';
    case 403:
        return 'permission-denied';
    case 404:
        return 'not-found';
    case 409:
        return 'aborted';
    case 429:
        return 'resource-exhausted';
    case 499:
        return 'cancelled';
    case 500:
        return 'internal';
    case 501:
        return 'unimplemented';
    case 503:
        return 'unavailable';
    case 504:
        return 'deadline-exceeded';
}
return 'unknown';

}

Thanks everyone for looking into this :)