0
\$\begingroup\$

How can the following method be flattened? I tried to pipe all the operators to the first pipe (from(this.fireAuth.currentUser)) but I lost access to the previous variables. It's an Angular HTTP interceptor that adds a Bearer Token when the user has logged in, the chain should continue as usual when the user has not logged in yet.

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const isApiUrl = request.url.startsWith(apiCloudURL);
    return from(this.fireAuth.currentUser).pipe(
      switchMap(user => {
        if (!user) return next.handle(request);
        return from(user.getIdToken()).pipe(
          switchMap(token => {
            if (isApiUrl) request = this.addToken(request, token);
            return next.handle(request).pipe(
              catchError(error => {
                if ( isApiUrl && token && error instanceof HttpErrorResponse && error.status === 403) {
                  return this.handle403Error(request, next);
                }
                else{
                  return throwError(error); 
                }
              })
            );
          })
        )
      })
    );
  }

Piping to the first observable... I have no access to the token var in the catchError block 😥

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const isApiUrl = request.url.startsWith(apiCloudURL);
    return from(this.fireAuth.currentUser).pipe(
      switchMap(user => {
        if (!user) return next.handle(request);
        return from(user.getIdToken());
      }),
      filter<string>(x => typeof(x) === "string"), //x can be string to HttpEvent<any>
      switchMap(token => {
        if (isApiUrl) request = this.addToken(request, token);
        return next.handle(request)
      }),
      catchError(error => {
        if ( isApiUrl && error instanceof HttpErrorResponse && error.status === 403) {
          return this.handle403Error(request, next);
        }
        else{
          return throwError(error); 
        }
      })
    );
  }

I'm also wondering what's happening in the scenario where user is falsy, does it mean the request will be canceled because it does not pass the filter?

Thanks.

\$\endgroup\$
1
  • \$\begingroup\$ Please edit your question so that the title describes the purpose of the code, rather than its mechanism. We really need to understand the motivational context to give good reviews. Thanks! \$\endgroup\$ – Toby Speight May 28 at 8:19
4
\$\begingroup\$

Your specific example can be simplified by treating the non-observables as simple variables, eg:

intercept(
  request: HttpRequest<any>,
  next: HttpHandler
): Observable<HttpEvent<any>> {
    
  const user = this.fireAuth.currentUser; 
  if (!user) return next.handle(request);

  const isApiUrl = request.url.startsWith(apiCloudURL);
  const token = user.getIdToken();

  if (isApiUrl) request = this.addToken(request, token);
  
  return next.handle(request).pipe(
    catchError(error => {
      if ( isApiUrl && 
            token && 
            error instanceof HttpErrorResponse && 
            error.status === 403) {
        return this.handle403Error(request, next);
      } else {
        return throwError(error); 
      }
    })
  )
}

However, in the general case your question uncovers a couple of interesting RxJS "patterns".

First let's ignore the path where there is no user. What you end up with is a sequence of requests (ie your switchMaps), but where the result of any earlier request is any later request. There is no completely clean solution to this in standard RxJS - a number of options are discussed in this article Aggregating RxJS Requests. Nesting is the most common solution, which was your starting point.

I have been involved in the implementation of a utility to address this scenario.

With your example the resultant code would be:

intercept(
  request: HttpRequest<any>,
  next: HttpHandler
): Observable<HttpEvent<any>> {
    
  const isApiUrl = request.url.startsWith(apiCloudURL);

  return concatJoin(
    from(this.fireAuth.currentUser),
    ([user]) => from(user.getIdToken),
    ([user, token]) => {
      if (isApiUrl) request = this.addToken(request, token);
      return next.handle(request).pipe(...),
    },
  ).pipe(
    // resultant value is [user, token, result] - we just want the result
    map([,,result]) => result
  );
}

Now returning to handle the case where there is no user, in your implementation you are right, the filter would cause it to never emit a value. There has to be a level of nesting:

intercept(
  request: HttpRequest<any>,
  next: HttpHandler
): Observable<HttpEvent<any>> {
    
  const isApiUrl = request.url.startsWith(apiCloudURL);

  return from(this.fireAuth.currentUser).pipe(
    switchMap(user => {  // or use the iif operator
      if (!user) return next.handle(request);

      return concatJoin(
        (from(user.getIdToken),
        ([token]) => {
          if (isApiUrl) request = this.addToken(request, token);
          return next.handle(request).pipe(...),
        },
      ).pipe(
        // resultant value is [token, result] - we just want the result
        map([,result]) => result
      );

    )
  )    
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.