Skip to content

Instantly share code, notes, and snippets.

@gund
Last active September 19, 2022 01:08
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gund/1072056dd3af86f084ae3a111946415b to your computer and use it in GitHub Desktop.
Save gund/1072056dd3af86f084ae3a111946415b to your computer and use it in GitHub Desktop.
Concept of Http Interceptor for Angular 2

First, we need to define interfaces with which we will work:

export interface Interceptable<T extends Interceptor<any, any>> {
  addInterceptor(interceptor: T): Interceptable<T>;
  removeInterceptor(interceptor: T): Interceptable<T>;
  clearInterceptors(interceptors?: T[]): Interceptable<T>;
}

export interface Interceptor<T, D> {
  (data: T): D;
}

export type RequestInterceptor = Interceptor<HttpRequestData, HttpRequestData>;
export type ResponseInterceptor = Interceptor<Observable<Response>, Observable<Response>>;

export interface HttpInterceptor {
  request(): Interceptable<RequestInterceptor>;
  response(): Interceptable<ResponseInterceptor>;
}

export interface HttpRequestData {
  url: string | Request;
  options?: RequestOptionsArgs;
  body?: any;
  cancelRequest?: boolean;
}

For us to easier to work we will extend intercafe of Http with our private API:

export interface InterceptableHttp extends Http {
  _interceptors: PrePostInterceptors;
  _interceptRequest(data: HttpRequestData): HttpRequestData;
  _interceptResponse(response: Observable<Response>): Observable<Response>;
}

export interface PrePostInterceptors {
  pre: RequestInterceptor[];
  post: ResponseInterceptor[];
}

Now we can extend Http with new feature:

@Injectable()
export class InterceptableHttpService extends Http implements InterceptableHttp {

  // noinspection JSUnusedGlobalSymbols
  _interceptors: PrePostInterceptors = {pre: [], post: []};

  constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions) {
    super(_backend, _defaultOptions);
  }

  request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options});
    return this._interceptResponse(super.request(req.url, req.options));
  }

  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options});
    return this._interceptResponse(super.get(<string>req.url, req.options));
  }

  post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options, body});
    return this._interceptResponse(super.post(<string>req.url, req.body, req.options));
  }

  put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options, body});
    return this._interceptResponse(super.put(<string>req.url, req.body, req.options));
  }

  // noinspection ReservedWordAsName
  delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options});
    return this._interceptResponse(super.delete(<string>req.url, req.options));
  }

  patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options, body});
    return this._interceptResponse(super.patch(<string>req.url, req.body, req.options));
  }

  head(url: string, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options});
    return this._interceptResponse(super.head(<string>req.url, req.options));
  }

  options(url: string, options?: RequestOptionsArgs): Observable<Response> {
    const req = this._interceptRequest({url, options});
    return this._interceptResponse(super.options(<string>req.url, req.options));
  }

  _interceptRequest(data: HttpRequestData): HttpRequestData {
    return this._interceptors.pre.reduce((d, i) => i(d), data);
  }

  _interceptResponse(response: Observable<Response>): Observable<Response> {
    return this._interceptors.post.reduce((o, i) => o.flatMap(_ => i(o)), response);
  }

}

And to manage interceptors we will create separate HttpInterceptor service (to follow SoC):

@Injectable()
export class HttpInterceptorService implements HttpInterceptor {

  private _preInterceptor = new InterceptableStore<RequestInterceptor>(this.http._interceptors.pre);
  private _postInterceptor = new InterceptableStore<ResponseInterceptor>(this.http._interceptors.post);

  constructor(@Inject(Http) private http: InterceptableHttp) {
  }

  request(): Interceptable<RequestInterceptor> {
    return this._preInterceptor;
  }

  response(): Interceptable<ResponseInterceptor> {
    return this._postInterceptor;
  }

}

Here in that service we encapsulated all work with interceptor's stores into local class InterceptableStore:

class InterceptableStore<T extends Interceptor<any, any>> implements Interceptable<T> {

  constructor(private store: T[]) {
  }

  addInterceptor(interceptor: T): Interceptable<T> {
    this.store.push(interceptor);
    return this;
  }

  removeInterceptor(interceptor: T): Interceptable<T> {
    this.store.splice(this.store.indexOf(interceptor), 1);
    return this;
  }

  clearInterceptors(interceptors: T[] = []): Interceptable<T> {
    if (interceptors.length > 0) {
      interceptors.forEach(i => this.removeInterceptor(i));
    } else {
      this.store.splice(0);
    }

    return this;
  }

}

That's it.

###Usage:

I tested this on simple example like:

    httpInterceptor.request().addInterceptor(data => {
      console.log(data);
      return data;
    });

    httpInterceptor.response().addInterceptor(res => {
      res.subscribe(r => console.log(r));
      return res;
    });

It is working as expected (for one request you will get 2 logs).
Next step is to move it to it's own npm package so I can publish it as ready-to-use lib.

Cheers,
And intercept your requests safely ;)

###Edit:

In order to replace original Http you have to add next setup to your providers collection of @NgModule:

{
  provide: Http,
  useFactory: (backend, defaultOptions) => new InterceptableHttpService(backend, defaultOptions),
  deps: [XHRBackend, RequestOptions]
}

Providing HttpInterceptorService is as simple as [HttpInterceptorService].

###Edit 2:

I just figured how to avoid all of the boilerplate overriding Http methods with ES6 Proxy'ies.
It will change a bit signature of Interceptor interface.
Here's my quick proof of concept:

First just define methods for interceptions:

@Injectable()
export class InterceptableHttpService extends Http {

  constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions) {
    super(_backend, _defaultOptions);
  }

  _interceptRequest(method: string, argArray) {
    console.log(method, argArray);
    return argArray;
  }

  _interceptResponse(method: string, observable) {
    console.log(method, 'response', observable);
    return observable;
  }

}

Then we will use so called InterceptableHttpProxy class as a proxy:

class InterceptableHttpProxy implements ProxyHandler<any> {

  private static _callStack: string[] = [];

  constructor(private service: InterceptableHttpService) {
  }

  apply(target: any, thisArg: any, argArray?: any): any {
    const method = InterceptableHttpProxy._callStack.pop();
    argArray = this.service._interceptRequest(method, argArray);
    return this.service._interceptResponse(method, this.service[method].apply(this.service, argArray));
  }

  get(target: any, p: PropertyKey, receiver: any): any {
    InterceptableHttpProxy._callStack.push(<string>p);
    return receiver;
  }

}

And after that we have to update provider factory function to utilize proxy class:

  {
    provide: Http,
    useFactory: (backend, defaultOptions) =>
      new Proxy(() => {
      }, new InterceptableHttpProxy(new InterceptableHttpService(backend, defaultOptions))),
    deps: [XHRBackend, RequestOptions]
  }

This will automatically intercept all calls to Http through our proxy and invoke _interceptRequest and _interceptResponse on InterceptableHttpService

@gund
Copy link
Author

gund commented Sep 26, 2016

Here is implementation of the concept described above:
https://github.com/gund/angular2-http-interceptor-test

Next up - is to move it to external NPM module so it can be easily utilized by anyone.

@gund
Copy link
Author

gund commented Sep 30, 2016

There's finally stable implementation also published to npm:
https://github.com/gund/ng2-http-interceptor

Check it out! =)

@christiaan-lombard
Copy link

Awesome, thanks!!

@nachodd
Copy link

nachodd commented Apr 22, 2017

Great work man! thanks!

@ralberts
Copy link

Huh, doesn't seem to like this code inside of doing the useFactory:

ERROR in Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function (position 61:25 in the original .ts file), resolving symbol AppModule in C:/Dev/Projects/cidne-reporting/app/assets/javascripts/ngx/app/app.module.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment