A long-requested feature for HttpClient is the ability to store and retrieve custom metadata for requests, especially in interceptors. This commit implements this functionality via a new context object for requests. Each outgoing HttpRequest now has an associated "context", an instance of the HttpContext class. An HttpContext can be provided when making a request, or if not then an empty context is created for the new request. This context shares its lifecycle with the entire request, even across operations that change the identity of the HttpRequest instance such as RxJS retries. The HttpContext functions as an expando. Users can create typed tokens as instances of HttpContextToken, and read/write a value for the key from any HttpContext object. This commit implements the HttpContext functionality. A followup commit will add angular.io documentation. PR Close #25751
140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.sonpCallbackContext
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {HttpHandler} from '@angular/common/http/src/backend';
|
|
import {HttpClient} from '@angular/common/http/src/client';
|
|
import {HttpContext, HttpContextToken} from '@angular/common/http/src/context';
|
|
import {HTTP_INTERCEPTORS, HttpInterceptor} from '@angular/common/http/src/interceptor';
|
|
import {HttpRequest} from '@angular/common/http/src/request';
|
|
import {HttpEvent, HttpResponse} from '@angular/common/http/src/response';
|
|
import {HttpTestingController} from '@angular/common/http/testing/src/api';
|
|
import {HttpClientTestingModule} from '@angular/common/http/testing/src/module';
|
|
import {TestRequest} from '@angular/common/http/testing/src/request';
|
|
import {Injectable, Injector} from '@angular/core';
|
|
import {TestBed} from '@angular/core/testing';
|
|
import {Observable} from 'rxjs';
|
|
import {map} from 'rxjs/operators';
|
|
|
|
const IS_INTERCEPTOR_C_ENABLED = new HttpContextToken<boolean|undefined>(() => undefined);
|
|
|
|
class TestInterceptor implements HttpInterceptor {
|
|
constructor(private value: string) {}
|
|
|
|
intercept(req: HttpRequest<any>, delegate: HttpHandler): Observable<HttpEvent<any>> {
|
|
const existing = req.headers.get('Intercepted');
|
|
const next = !!existing ? existing + ',' + this.value : this.value;
|
|
req = req.clone({setHeaders: {'Intercepted': next}});
|
|
return delegate.handle(req).pipe(map(event => {
|
|
if (event instanceof HttpResponse) {
|
|
const existing = event.headers.get('Intercepted');
|
|
const next = !!existing ? existing + ',' + this.value : this.value;
|
|
return event.clone({headers: event.headers.set('Intercepted', next)});
|
|
}
|
|
return event;
|
|
}));
|
|
}
|
|
}
|
|
|
|
class InterceptorA extends TestInterceptor {
|
|
constructor() {
|
|
super('A');
|
|
}
|
|
}
|
|
|
|
class InterceptorB extends TestInterceptor {
|
|
constructor() {
|
|
super('B');
|
|
}
|
|
}
|
|
|
|
class InterceptorC extends TestInterceptor {
|
|
constructor() {
|
|
super('C');
|
|
}
|
|
|
|
intercept(req: HttpRequest<any>, delegate: HttpHandler): Observable<HttpEvent<any>> {
|
|
if (req.context.get(IS_INTERCEPTOR_C_ENABLED) === true) {
|
|
return super.intercept(req, delegate);
|
|
}
|
|
return delegate.handle(req);
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
class ReentrantInterceptor implements HttpInterceptor {
|
|
constructor(private client: HttpClient) {}
|
|
|
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
return next.handle(req);
|
|
}
|
|
}
|
|
|
|
describe('HttpClientModule', () => {
|
|
let injector: Injector;
|
|
beforeEach(() => {
|
|
injector = TestBed.configureTestingModule({
|
|
imports: [HttpClientTestingModule],
|
|
providers: [
|
|
{provide: HTTP_INTERCEPTORS, useClass: InterceptorA, multi: true},
|
|
{provide: HTTP_INTERCEPTORS, useClass: InterceptorB, multi: true},
|
|
{provide: HTTP_INTERCEPTORS, useClass: InterceptorC, multi: true},
|
|
],
|
|
});
|
|
});
|
|
it('initializes HttpClient properly', done => {
|
|
injector.get(HttpClient).get('/test', {responseType: 'text'}).subscribe((value: string) => {
|
|
expect(value).toBe('ok!');
|
|
done();
|
|
});
|
|
injector.get(HttpTestingController).expectOne('/test').flush('ok!');
|
|
});
|
|
it('intercepts outbound responses in the order in which interceptors were bound', done => {
|
|
injector.get(HttpClient)
|
|
.get('/test', {observe: 'response', responseType: 'text'})
|
|
.subscribe(() => done());
|
|
const req = injector.get(HttpTestingController).expectOne('/test') as TestRequest;
|
|
expect(req.request.headers.get('Intercepted')).toEqual('A,B');
|
|
req.flush('ok!');
|
|
});
|
|
it('intercepts outbound responses in the order in which interceptors were bound and include specifically enabled interceptor',
|
|
done => {
|
|
injector.get(HttpClient)
|
|
.get('/test', {
|
|
observe: 'response',
|
|
responseType: 'text',
|
|
context: new HttpContext().set(IS_INTERCEPTOR_C_ENABLED, true)
|
|
})
|
|
.subscribe(value => done());
|
|
const req = injector.get(HttpTestingController).expectOne('/test') as TestRequest;
|
|
expect(req.request.headers.get('Intercepted')).toEqual('A,B,C');
|
|
req.flush('ok!');
|
|
});
|
|
it('intercepts inbound responses in the right (reverse binding) order', done => {
|
|
injector.get(HttpClient)
|
|
.get('/test', {observe: 'response', responseType: 'text'})
|
|
.subscribe((value: HttpResponse<string>) => {
|
|
expect(value.headers.get('Intercepted')).toEqual('B,A');
|
|
done();
|
|
});
|
|
injector.get(HttpTestingController).expectOne('/test').flush('ok!');
|
|
});
|
|
it('allows interceptors to inject HttpClient', done => {
|
|
TestBed.resetTestingModule();
|
|
injector = TestBed.configureTestingModule({
|
|
imports: [HttpClientTestingModule],
|
|
providers: [
|
|
{provide: HTTP_INTERCEPTORS, useClass: ReentrantInterceptor, multi: true},
|
|
],
|
|
});
|
|
injector.get(HttpClient).get('/test').subscribe(() => {
|
|
done();
|
|
});
|
|
injector.get(HttpTestingController).expectOne('/test').flush('ok!');
|
|
});
|
|
});
|