How can I allow my HTTP request to continue propagating through my app after a caught failure?

Viewed 81

I'm using Ionic 2 which sits on top of Angular 2.

I am using a service in my app to handle all API requests. Some of those requests carry an Authorization header. My API can return a 401 if the auth fails, and I am currently handling that by showing an error to the user and navigating them to the login screen. That all works great and I publish an event for these errors which I subscribe to elsewhere to handle the logic (as I can't use navigation controllers in a service).

Code for api.js service:

@Injectable()
export class Api {
  base_url: string = 'https://***.com';
  url: string = this.base_url + '/api/v1';
  authurl: string = this.base_url + '/oauth/token';
  grant_type: string = 'password';
  client_id: string = '1';
  client_secret: string = '***';
  access_token: string;

  constructor(
    public http: Http, 
    private storage: Storage,
    public events: Events) {

    // Grab access token and store it
    storage.get('access_token').then((val) => {
      this.access_token = val;
    });
  }

  // Performs a GET request with auth headers
  get(endpoint: string, params?: any) {
    if(!params) {
      params = [];
    }
    params['grant_type'] = this.grant_type;
    params['client_id'] = this.client_id;
    params['client_secret'] = this.client_secret;

    let headers: Headers = this.getHeaders();

    return this.getApiToken().flatMap(data => {

      headers.append('Authorization', 'Bearer ' + data);

      let options = new RequestOptions({ headers: headers });

      // Support easy query params for GET requests
      if (params) {
        let p = new URLSearchParams();
        for (let k in params) {
          p.set(k, params[k]);
        }
        // Set the search field if we have params and don't already have
        // a search field set in options.
        options.search = !options.search && p || options.search;
      } 

      return this.http.get(this.url + '/' + endpoint, options)
        .catch((error: any) => {
          if (error.status === 500) {
            this.events.publish('api:generalError', error);
            return Observable.throw(new Error(error.status));
          }
          else if (error.status === 401) {
            this.events.publish('api:unauthorized', error);
            return Observable.throw(new Error(error.status));
          }
        });
    }).share();
  }
}

The problem is down to the 'Loading...' dialog that I show to the user while the get request is taking place. Before the method is called I create a loading dialog and dismiss it on success or failure. The problem is that I don't have any scope of this inside api.js in order to dismiss it when a 401 or 500 is caught.

Here's a sample of my logic around this:

let loader = this.loadingCtrl.create({
  content: "Please wait..."
});
loader.present();

this.trainingProgramme.get_programmes()
  .map(res => res.json())
  .subscribe((res) => {

  this.currentItems = res.training_programmes;

}, (err) => {
  // Error
  console.log(err);
}, () => {
  loader.dismiss();
});

I don't think it's important, but I also have a service for each entity which in turn calls api.js service. In the above example it's this.trainingProgramme, which looks like this:

  get_programmes() {
    let seq = this.api.get('training-programmes');

    seq
      .subscribe();

    return seq;
  }

I thought the way I approached this was all correct, however I can't see a way I can handle the 'Loading' issue.

Is there a way I can have the get method continue within my app even after an error has been caught, so that my loader.dismiss() code is ran within the correct scope?

I really don't want to have to use the loader inside the service (not sure I'm even able to?) as this seems like bad design, and I don't want to always show a loader, so it belongs in the controller.

1 Answers
Related