Skip to content

Commit ed2b717

Browse files
alxhubjasonaden
authored andcommitted
fix(common): allow HttpInterceptors to inject HttpClient (#19809)
Previously, an interceptor attempting to inject HttpClient directly would receive a circular dependency error, as HttpClient was constructed via a factory which injected the interceptor instances. Users want to inject HttpClient into interceptors to make supporting requests (ex: to retrieve an authentication token). Currently this is only possible by injecting the Injector and using it to resolve HttpClient at request time. Either HttpClient or the user has to deal specially with the circular dependency. This change moves that responsibility into HttpClient itself. By utilizing a new class HttpInterceptingHandler which lazily loads the set of interceptors at request time, it's possible to inject HttpClient directly into interceptors as construction of HttpClient no longer requires the interceptor chain to be constructed. Fixes #18224. PR Close #19809
1 parent fad99cc commit ed2b717

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

packages/common/http/src/module.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,41 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Inject, ModuleWithProviders, NgModule, Optional} from '@angular/core';
9+
import {Injectable, Injector, ModuleWithProviders, NgModule, Optional} from '@angular/core';
10+
import {Observable} from 'rxjs/Observable';
1011

1112
import {HttpBackend, HttpHandler} from './backend';
1213
import {HttpClient} from './client';
1314
import {HTTP_INTERCEPTORS, HttpInterceptor, HttpInterceptorHandler, NoopInterceptor} from './interceptor';
1415
import {JsonpCallbackContext, JsonpClientBackend, JsonpInterceptor} from './jsonp';
16+
import {HttpRequest} from './request';
17+
import {HttpEvent} from './response';
1518
import {BrowserXhr, HttpXhrBackend, XhrFactory} from './xhr';
1619
import {HttpXsrfCookieExtractor, HttpXsrfInterceptor, HttpXsrfTokenExtractor, XSRF_COOKIE_NAME, XSRF_HEADER_NAME} from './xsrf';
1720

21+
/**
22+
* An `HttpHandler` that applies a bunch of `HttpInterceptor`s
23+
* to a request before passing it to the given `HttpBackend`.
24+
*
25+
* The interceptors are loaded lazily from the injector, to allow
26+
* interceptors to themselves inject classes depending indirectly
27+
* on `HttpInterceptingHandler` itself.
28+
*/
29+
@Injectable()
30+
export class HttpInterceptingHandler implements HttpHandler {
31+
private chain: HttpHandler|null = null;
1832

33+
constructor(private backend: HttpBackend, private injector: Injector) {}
34+
35+
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
36+
if (this.chain === null) {
37+
const interceptors = this.injector.get(HTTP_INTERCEPTORS, []);
38+
this.chain = interceptors.reduceRight(
39+
(next, interceptor) => new HttpInterceptorHandler(next, interceptor), this.backend);
40+
}
41+
return this.chain.handle(req);
42+
}
43+
}
1944

2045
/**
2146
* Constructs an `HttpHandler` that applies a bunch of `HttpInterceptor`s
@@ -118,13 +143,7 @@ export class HttpClientXsrfModule {
118143
],
119144
providers: [
120145
HttpClient,
121-
// HttpHandler is the backend + interceptors and is constructed
122-
// using the interceptingHandler factory function.
123-
{
124-
provide: HttpHandler,
125-
useFactory: interceptingHandler,
126-
deps: [HttpBackend, [new Optional(), new Inject(HTTP_INTERCEPTORS)]],
127-
},
146+
{provide: HttpHandler, useClass: HttpInterceptingHandler},
128147
HttpXhrBackend,
129148
{provide: HttpBackend, useExisting: HttpXhrBackend},
130149
BrowserXhr,

packages/common/http/test/module_spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import 'rxjs/add/operator/map';
1010

11-
import {Injector} from '@angular/core';
11+
import {Injectable, Injector} from '@angular/core';
1212
import {TestBed} from '@angular/core/testing';
1313
import {Observable} from 'rxjs/Observable';
1414

@@ -47,6 +47,15 @@ class InterceptorB extends TestInterceptor {
4747
constructor() { super('B'); }
4848
}
4949

50+
@Injectable()
51+
class ReentrantInterceptor implements HttpInterceptor {
52+
constructor(private client: HttpClient) {}
53+
54+
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
55+
return next.handle(req);
56+
}
57+
}
58+
5059
{
5160
describe('HttpClientModule', () => {
5261
let injector: Injector;
@@ -84,5 +93,16 @@ class InterceptorB extends TestInterceptor {
8493
});
8594
injector.get(HttpTestingController).expectOne('/test').flush('ok!');
8695
});
96+
it('allows interceptors to inject HttpClient', (done: DoneFn) => {
97+
TestBed.resetTestingModule();
98+
injector = TestBed.configureTestingModule({
99+
imports: [HttpClientTestingModule],
100+
providers: [
101+
{provide: HTTP_INTERCEPTORS, useClass: ReentrantInterceptor, multi: true},
102+
],
103+
});
104+
injector.get(HttpClient).get('/test').subscribe(() => { done(); });
105+
injector.get(HttpTestingController).expectOne('/test').flush('ok!');
106+
});
87107
});
88108
}

0 commit comments

Comments
 (0)