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
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.