4

How to handle cloned request with new headers?

I am trying to perform a cloned request in the new interceptors with the new token generated after the first request fail!

  • If err 401 -> refresh token
  • send back a copy of the previous request with the new headers

Here what I tried:

  refreshSessionToken() {
    return this.http.post('/connect', {})
      .map(res => {
        const token = res.json();
        localStorage.setItem('token', token);
      return token;
      });
  }


// Get the headers
const headers = getAuthenticationHeaders();
const reqMain = req.clone({headers: headers});
return next.handle(reqMain).catch(err => {

       this.refreshSessionToken()
            .subscribe(token => {
                const t = token.token_id;
                const clonedReq = req.clone({headers: req.headers.set('access-token', t)});

                return next.handle(clonedReq);
                    });
         })

getting a log of the clonedReq I can see that the token is refreshed, but the request (clonedReq) inside the subscribe is not performed, why?!

I tried other approaches on how to refresh the JWT but it doesn't seem to work in my case, any help on how to deal with it?

Thanks!

What do I expect to be my result?

  • Send 1# HTTP request
  • 1# request fail
  • Refresh token (get/set token)
  • Clone previous request and add refreshed token
  • Send 2# HTTP request
  • 2# request success

Similiar issues:

http-interceptor-refresh-jwt-token

dipak
  • 2,011
  • 2
  • 17
  • 24
39ro
  • 862
  • 1
  • 10
  • 23
  • 1
    if the return next.handle(clonedReq); is observable(Which i doubt is) then you will need to subscribe it.. – dipak Aug 10 '17 at 16:29
  • @Dipak make sense! Thanks :D :D Now the cloned request is fired and the 2# request result to success. - The log inside the subscribe is fired twice (the HTTP request is just sent once.. so it's good), but I am not really sure why is it happening? `next.handle(clonedReq) .subscribe(() => console.log('Cloned Request Fired Twice!'));` – 39ro Aug 10 '17 at 17:50
  • The inner return is culprit and your way of doing this is mess... Though it may work but I don't like it. Their is lot better way...i will post answers once i get home... Wait... – dipak Aug 10 '17 at 17:57
  • @Dipak Sound amazing, can't wait for it! – 39ro Aug 10 '17 at 17:59

3 Answers3

4

Following generalized method could be used to intercept as well as add/remove additional information to call and responses.

All right, Here the complete code.

InterceptedHttp.ts


import { Injectable } from "@angular/core";
import { RequestOptions, Http, Headers, Response, RequestMethod, URLSearchParams } from "@angular/http";
import { Observable, Observer } from 'rxjs/Rx';

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import "rxjs/add/operator/mergeMap";

@Injectable()
export class InterceptedHttp {

    constructor(private http: Http) { }

    getRequestOption(method: RequestMethod | string, data?: any, params?: any): RequestOptions {
        let options = new RequestOptions();
        options.headers = new Headers();
        //options.headers.append('Content-Type', 'application/json');
        options.method = method;

        let token: string = localStorage.getItem('token');
        //if (token) options.headers.append('Authorization', 'Bearer ' + token);

        if (data) options.body = data;

        if (params) {
            options.search = new URLSearchParams();
            let keys: string[] = Object.keys(params);

            keys.forEach((key, index) => {
                options.search.set(key, params[key]);
            });
        }

        return options;
    }

    refreshSessionToken(): Observable<string> {
        //Put some user identification data
        let userData: any = {id: 'abc'};
        return this.http.post('/refreshToken', userData)
            .map(res => {
                let token = res.json();
                localStorage.setItem('token', token);
                return token;
            });
    }

    getApiResponse<T>(url: string, method: RequestMethod | string, data?: Object): Observable<T> {
        let op1: RequestOptions = this.getRequestOption(method, data);
       return  this.http.request(url, op1)
            .catch((err) => {
                // UnAuthorised, 401
                if (err.status == 401) {
                    return this.refreshSessionToken().flatMap(t => {
                        let op2 = this.getRequestOption(method, data);
                        return this.http.request(url, op2);
                    });
                }
                throw err;
            })
            .map((response: Response) => {
                let ret: T = response.json();
                return ret;
            });
    }

    get<T>(url: string): Observable<T> {
        return this.getApiResponse<T>(url, RequestMethod.Get);
    }

    post<T, R>(url: string, body: T): Observable<R> {
        return this.getApiResponse<R>(url, RequestMethod.Post, body);
    }

    put<T, R>(url: string, body: T): Observable<R> {
        return this.getApiResponse<R>(url, RequestMethod.Put, body);
    }

    delete<T>(url: string): Observable<T> {
        return this.getApiResponse<T>(url, RequestMethod.Delete);
    }
}

DataService.ts

    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs/Observable';
    import { User } from './User';
    import { InterceptedHttp } from './http.interceptor';

    @Injectable()
    export class DataService {
        constructor(private apiHandler: InterceptedHttp) { }

        getAll(): Observable<User[]> {
            return this.apiHandler.get<User[]>('http://mocker.egen.io/users');
        }
    }

User.ts

    export class User {
        id?: number;

        firstName?: string;

        lastName?: string;
    }

AppComponent.ts

    import { Component } from '@angular/core';
    import { DataService } from './user-data.service';
    import { User } from './User';

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'app works!';
      users: User[];

      constructor(dataService: DataService){
        dataService.getAll().subscribe((u) => {
          this.users = u;
        });
      }  
    }

app.component.html

<h1>
  <table>
    <tr *ngFor="let item of users; let i = index">
      <td> {{item.firstName}} </td>
    </tr>
  </table>
</h1>
dipak
  • 2,011
  • 2
  • 17
  • 24
  • I need to say that I am not a real fan of promises, maybe because I didn't find enough time or scenarios to play/study with that! I didn't really catch why everything is wrapped in a getApiResponse instead of the intercept... Is it like a separate method that it is fired to a static endpoint to check if the token is still valid before to send to the Api a real request? – 39ro Aug 11 '17 at 11:34
  • Well, its obvious choice to use observable and promise in Angular apps. The http.get post and all returns promise... You need to understand how to construct promise...that should do the job. The example I gave exactly match your requirements. Believe it or not you are using promise with/without you knowledge – dipak Aug 11 '17 at 14:15
  • 1
    Updated with working example without Promise... https://github.com/dvbava/Retry-HttpInterceptor-Angular just for documentation. – dipak Aug 15 '17 at 22:33
1

Here is the solution which I got working with the latest version of Angular (7.0.0) and rxjs (6.3.3). Hope it helps.

export class SessionRecoveryInterceptor implements HttpInterceptor {
  constructor(
    private readonly store: StoreService,
    private readonly sessionService: AuthService
  ) {}

  private _refreshSubject: Subject<any> = new Subject<any>();

  private _ifTokenExpired() {
    this._refreshSubject.subscribe({
      complete: () => {
        this._refreshSubject = new Subject<any>();
      }
    });
    if (this._refreshSubject.observers.length === 1) {
      // Hit refresh-token API passing the refresh token stored into the request
      // to get new access token and refresh token pair
      this.sessionService.refreshToken().subscribe(this._refreshSubject);
    }
    return this._refreshSubject;
  }

  private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {
    return (
      error.status &&
      error.status === 401 &&
      error.error &&
      error.error.message === "TokenExpired"
    );
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {
      return next.handle(req);
    } else {
      return next.handle(req).pipe(
        catchError((error, caught) => {
          if (error instanceof HttpErrorResponse) {
            if (this._checkTokenExpiryErr(error)) {
              return this._ifTokenExpired().pipe(
                switchMap(() => {
                  return next.handle(this.updateHeader(req));
                })
              );
            } else {
              return throwError(error);
            }
          }
          return caught;
        })
      );
    }
  }

  updateHeader(req) {
    const authToken = this.store.getAccessToken();
    req = req.clone({
      headers: req.headers.set("Authorization", `Bearer ${authToken}`)
    });
    return req;
  }
}

I answered a similar question here. You can also have a read at my article here for the understanding of the code.

Samarpan
  • 913
  • 5
  • 12
0

I answered a similar question here

You can't pass the cloned request into the next handler. Instead, use the HttpClient to re-try the cloned request.

this.http.request(clonedReq).subscribe(......);