4

I'm using RC6 and I'm trying to figure out how to catch HTTP errors - auth errors in particular - in the entire application.

There are a number of posts that describe how to extend the Http class with a custom class, but I'm not sure how to register the new class exactly as it appears the syntax has changed with the the recent ngModule changes.

Here's the class (with all relevant imports added):

@Injectable()
export class InterceptedHttp extends Http {

 constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
  super( backend, defaultOptions);
 }

 request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
  console.log('request...');
  return super.request(url, options);
 }

 get(url: string, options?: RequestOptionsArgs): Observable<Response> {
  console.log('get...');
  return super.get(url,options);
 }
}

I thought I'd be able to do the following in the providers section of @ngModule:

 imports: [ HttpModule, ... ],
 providers: [
    ... 

    InterceptedHttp,
    {provide: Http, useClass: InterceptedHttp },
    ConnectionBackend
 ],

but this just gets me a bunch of missing module errors:

ERROR in [default] C:/WebConnectionProjects/AlbumViewer/Web/src/app/app.module.ts:64:10
Argument of type '{ imports: (ModuleWithProviders | typeof BrowserModule)[]; declarations: (typeof AlbumList | type...' is not assignable to parameter of type 'NgModuleMetadataType'.
  Types of property 'providers' are incompatible.
  Type '(typeof ConnectionBackend | typeof Album | typeof Artist | typeof Track | typeof AppConfiguration...' is not assignable to type 'Provider[]'.
  Type 'typeof ConnectionBackend | typeof Album | typeof Artist | typeof Track | typeof AppConfiguration ...' is not assignable to type 'Provider'.
  Type 'typeof ConnectionBackend' is not assignable to type 'Provider'.
  Type 'typeof ConnectionBackend' is not assignable to type 'FactoryProvider'.
  Property 'provide' is missing in type 'typeof ConnectionBackend'.

removing the added lines and everything works.

So, how do I register a custom Http class?

Rick Strahl
  • 17,302
  • 14
  • 89
  • 134
  • Pretty much same question was just asked yesterday. And my answer there is kind of close to what @BeetleJuice suggested below. Little more robust. http://stackoverflow.com/questions/39422218/angular-2-http-response-generic-error-handler/39422251 – rook Sep 12 '16 at 01:09

2 Answers2

3

My approach to this has been different. I created an HTTPService class that interacts with the built-in Http, instead of extending Http.

@Injectable()
export class HttpService{
    constructor(private http:Http){}

    /** Wrapper for Http.get() that intercepts requests and responses */
    get(url:string, options?:RequestOptions):Observable<any>{

        //pre-screen the request (eg: to add authorization token)
        options = this.screenRequest(options);

        return this.http.get(url,options)
            .map(res => res.json()) //my back-end return a JSON. Unwrap it
            .do(res => this.screenResponse(res)) // intercept response
            .catch(res => this.handleError(res));// server returned error status
    }

    /** similar to the above; a wrapper for Http.post() */
    post(url:string, body:string ,options?:RequestOptions):Observable<any>{}

    /** edits options before the request is made. Adds auth token to headers.*/
    screenOptions(options?:RequestOptions):RequestOptions{}

    /** Called with server's response. Saves auth token from the server */
    screenResponse(jsonResponse:any){}

    /** Called when server returns a 400-500 response code */
    handleError(response:Response){}        
}

So my code never calls Angular's Http directly. Instead, I call HttpService.get().

BeetleJuice
  • 39,516
  • 19
  • 105
  • 165
  • Yeah I thought of that as well and that's probably not a bad way to go. Just like with the other solution I hate that there's no single point of intercept for request and response operations. – Rick Strahl Sep 12 '16 at 01:07
  • 1
    Ok so I tried this, and somehow when I implement the `.catch()` I cannot return the observable. I get a type mismatch:zone.js:344 Unhandled Promise rejection: this.http.get(...).catch is not a function. here's a gist of the actual code: https://gist.github.com/RickStrahl/204de6f9e6607b79010f5e7648b0d1a7 – Rick Strahl Sep 12 '16 at 03:54
  • I can't make out which function is returning at the end of your gist, but why are you calling `this.http.get`? You should call your wrapper functions ( eg: `this.get()`) and only *it* should have access to the Angular `Http` service. – BeetleJuice Sep 12 '16 at 06:10
2

I took a different approach and extended the XHRBackend and so far it's handled all my needs.

export class CoreXHRBackend extends XHRBackend {

    constructor(xhr:BrowserXhr, opts:ResponseOptions, strat:XSRFStrategy, public alerts:Alerts) {
        super(xhr, opts, strat);
    }

    createConnection(request:Request) {
        let xhr = super.createConnection(request);

        /**
         * Global error handler for http requests
         */
        xhr.response = xhr.response.catch((error:Response) => {

            if (error.status === 401 && window.location.pathname !== '/') {
                this.alerts.clear().flash('You are not authorized to access that page', 'danger');
                window.location.href = '/';
            }

            if (error.status === 404) {
                this.alerts.clear().error('Sorry, we couldn\'t find that...');
            }

            // Validation errors or other list of errors
            if (error.status === 422) {
                var messages = error.json();
                Object.keys(messages).map(k => this.alerts.error(messages[k]));
            }

            if (error.status === 500) {
                this.alerts.clear().error('Sorry Something Went Wrong, Try Again Later!');
            }

            return Observable.throw(error);
        });

        return xhr;
    }
}

I also needed to inject my custom alerts service and the constructor isn't injectable so I handled that in my module like this...

export class CoreModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: CoreModule,
            providers: [
                Alerts,
                {
                    provide: XHRBackend,
                    useFactory: (xhr, opts, strat, alerts) => {
                        return new CoreXHRBackend(xhr, opts, strat, alerts);
                    },
                    deps: [ BrowserXhr, ResponseOptions, XSRFStrategy, Alerts ],
                }
            ],
        };
    }
}
Rob
  • 12,659
  • 4
  • 39
  • 56
  • I like the idea of this but I can't seem to get it to work when setting this up in AppModule. I'm adding the providers as you have here in the root module, but I get: compiler.umd.js:8121Uncaught Error: Provider parse errors: Cannot instantiate cyclic dependency! Http: in NgModule AppModule - I inject a different service but otherwise same logic. – Rick Strahl Sep 12 '16 at 02:05
  • Are you importing that module into more than one module? – Rob Sep 12 '16 at 02:21
  • Hmmm... I'm importing http into the AppModule. But I guess one of these other ones is importing it too and that's the problem? If that's the case, this seems crazy since we can't control what other modules use. – Rick Strahl Sep 12 '16 at 03:54
  • Well i can't see your code but I'm guessing you need a singleton implementation of the XHRBackend provider. That's why I'm using `forRoot` in a separate module and ensuring i'm only providing a single instance. – Rob Sep 12 '16 at 03:56
  • My AppModule uses `imports: [ CoreModule.forRoot() ]` – Rob Sep 12 '16 at 03:58