Search code examples
ajaxtypescriptpromiseaurelia

Trapping errors in Aurelias HTTP client


Hi All (Especially the Aurelia core team hanging about round here)

I have an aurelia app using the "aurelia-http-client" to make requests to my back end API.

My back end API is a C# based service running on Nancy.

In my front end Iv'e abstracted the http client out to my own network lib as follows:

import { inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
import { HttpClient } from 'aurelia-http-client';
import environment from './environment';

@inject(HttpClient, Router)
export default class httpservice {

  private http: HttpClient = null;
  private router: Router = null;
  private authService: any = null;
  private authToken: string = "";

  constructor(HttpClient, Router) {

    this.http = HttpClient;
    this.router = Router;

    HttpClient.configure(http => {
      http.withBaseUrl(environment.servicebase);
    });

  }

  public setAuthService(authService: any) {
    this.authService = authService;
  }

  public get(url: string, authObject?: any): any {

    let myAuth = this.authService ? this.authService : authObject;
    let myToken = "";
    if (myAuth) {
      myToken = myAuth.getAuthToken();
    }

    let self = this;
    let client = this.http
      .createRequest(url)
      .asGet()
      .withHeader("AuthenticationToken", myToken)
      .withInterceptor({
        responseError(responseError) {
          console.log(responseError);
          if (responseError.statusCode === 401) {
            if (myAuth) {
              myAuth.destroySession();
            }
          }
          if (responseError.statusCode === 404) {
            self.router.navigateToRoute("missing");
          }
          return responseError;
        }
      });

    return client;
  }

  public post(url: string, postData: any, authObject?: any): any {

    let myAuth = this.authService ? this.authService : authObject;
    let myToken = "";
    if (myAuth) {
      myToken = myAuth.getAuthToken();
    }

    let self = this;
    let client = this.http
      .createRequest(url)
      .asPost().withContent(postData)
      .withHeader("AuthenticationToken", myToken)
      .withInterceptor({
        responseError(responseError) {
          console.log(responseError);
          if (responseError.statusCode === 401) {
            if (myAuth) {
              myAuth.destroySession();
            }
          }
          if (responseError.statusCode === 404) {
            self.router.navigateToRoute("missing");
          }
          return responseError;
        }
      });

    return client;
  }

}

and I then use this in my other modules/classes as follows:

import { Aurelia, inject } from 'aurelia-framework';
import HttpService from './httpservice';
import environment from './environment';
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator, Aurelia, HttpService)
export default class Authservice {

  public http: HttpService = null;
  public app: Aurelia = null;
  public ea: EventAggregator = null;
  public authToken: any = null;

  private loginUrl: string = "";
  private logoutUrl: string = "";
  private checkUrl: string = "";

  constructor(eventAggregator, aurelia, httpService) {

    this.http = httpService;
    this.app = aurelia;
    this.ea = eventAggregator;

    this.loginUrl = "/login";
  }

  public getAuthToken() {
if (!sessionStorage[environment.tokenname] || 
   (sessionStorage[environment.tokenname] == null)) {
      return null;
   }
   return sessionStorage[environment.tokenname];
  }

  public login(loginName, password) {

    let postData = {
      loginName: loginName,
      password: password
    };

    let client = this.http.post(this.loginUrl, postData);

    client.send()
      .then((response) => response.content)
      .then((data) => {

        if (data.error) {
          this.ea.publish("loginMessage", { message: data.errorMessage });
          return;
        }

        if (data.authenticationFailed) {
          this.ea.publish("loginMessage", { message: "Invalid user name and/or password supplied." });
          return;
        }

        if (data.accountSuspended) {
          this.ea.publish("loginMessage", { message: "Your account has been suspended, please contact support." });
          return;
        }

        sessionStorage[environment.tokenname] = data.token;
        sessionStorage["displayedLoginName"] = data.displayName;
        location.assign('#/');
        this.app.setRoot('app');

      })
      .catch(() =>
      {
        debugger;
        alert("Something bad happened trying to connect to server.");
      });
  }

  public isAuthenticated() {
    // TODO: hook this up to check auth token validity via rest call???
    let token = this.getAuthToken();
    return token !== null;
  }

}

enum LoginStates {
  LoginValid = 0,
  BadUserNameOrPassword,
  AccountSuspended
   }

Please note I've stripped some of the code out of the auth library to reduce confusion

In general ALL of this works well. The interceptors get triggered when 401s and 404s occur, and if I add a 500 that get's handled too, so where all good there.

The problem I have is handling communication failures.

As you can see in the login routine, I have a catch following the then.

I expected that if the server couldn't be reached or some other base communications failure occurred, that this catch would trigger rather than the "then" and thus allow me to handle the error, but instead it does not.

What I get instead is this in the console:

Aurelia HTTP client error

Worse still, my login routine doesn't abort, it actually succeeds and allows the logged in page to be shown.

It seems that while the library is making the OPTIONS call (Which is when this error occurs) none of my user code is taken into account.

The OPTIONS call is required for successful pre-flight/ajax requests, so stopping this happening is not an option, and I feel that if the OPTIONS call did not abort, but made it to the POST call,t hat my error handling would then be taken into consideration.

It seems silly to be not able to trap errors like this, especially in today's mobile world where a device may be out of coverage or temporarily offline.

If anyone has any thoughts on how this can be solved, I'd love to hear them.

Update 1

My problem seems to be related to this one: aurelia-fetch-client - a promise was rejected with a non-error: [object Response]

However, I'm not using "useStandardConfiguration()" which is apparently the cause for that case. I'm also not using the fetch client, however I do note that the API in both clients is practically the same, so I wonder if the underlying code is also similar.


Solution

  • Ok.... so, after a long hard afternoon of head scratching and hair pulling, it turns out, the whole thing is actually linked to a reported issue with the "BlueBird promises library" which is what aurelia uses to manage it's promises.

    The link to the issue with BlueBird can be found here: https://github.com/petkaantonov/bluebird/issues/990

    It's not specifically an issue according to the BB dev's but to many folks encountering it, it sure looks like one.

    The bottom line is that the library is not designed to throw the errors generated directly by it (As the example on the issue page shows)

    The correct way according to the BB team, is to either throw a new error completely, or derive a new instance from the one passed to the promise, and alter the parameters to it before then re-throwing it.

    Of course, because of the abstraction in Aurelia, this is not an option for most of us, unless we want to go about changing the http client library code.

    Some of the marks for this need to go to "TheBlueFox" for His/Her comments above.

    The solution ended up being something like the following:

    import { inject } from 'aurelia-framework';
    import { Router } from 'aurelia-router';
    import { HttpClient, Interceptor } from 'aurelia-http-client';
    import environment from './environment';
    import Debugger = require("_debugger");
    
    @inject(HttpClient, Router)
    export default class httpservice {
    
      private http: HttpClient = null;
      private router: Router = null;
      private authService: any = null;
      private authToken: string = "";
      private myInterceptors: Interceptor;
    
      constructor(HttpClient, Router) {
    
        this.http = HttpClient;
        this.router = Router;
    
        HttpClient.configure(http => {
          http.withBaseUrl(environment.servicebase);
          http.withInterceptor(new HttpInterceptors());
        });
    
      }
    
      public setAuthService(authService: any) {
        this.authService = authService;
      }
    
      public get(url: string, authObject?: any): any {
    
        let myAuth = this.authService ? this.authService : authObject;
        let myToken = "";
        if (myAuth) {
          myToken = myAuth.getAuthToken();
        }
    
        let client = this.http
          .createRequest(url)
          .asGet()
          .withHeader("AuthenticationToken", myToken);
    
        return client;
      }
    
      public post(url: string, postData: any, authObject?: any): any {
    
        let myAuth = this.authService ? this.authService : authObject;
        let myToken = "";
        if (myAuth) {
          myToken = myAuth.getAuthToken();
        }
    
        let self = this;
    
        let client = this.http
          .createRequest(url)
          .asPost().withContent(postData)
          .withHeader("AuthenticationToken", myToken);
    
        return client;
      }
    
    }
    
    class HttpInterceptors implements Interceptor {
    
      responceError(error)
      {
    
        if (error.statusCode === 0) {
          throw new Error("Could not contact server");
        }
    
        if (error.statusCode === 401) {
          // do auth handling here
        }
    
        if (error.statusCode === 404) {
          // do 404 handling here
        }
    
        return error;
    
      }
    }
    

    The magic is in the HttpInterceptors class attached to the bottom of my HttpService. You should be able to see a check for a status code of 0, and that the actual action performed here is to throw a new error.

    It's the action of this new error being thrown that then causes the "catch" in the actual call to the http client to be caught.

    If you don't throw at that point, then everything just falls apart and you get the scenario seen in my original question post, throw and you get to catch it and deal with it in user code.

    This way of doing things is also apparent in the aurelia-fetch-client too, as that works in a broadly similar way, using the BlueBird promise library.