Skip to content

Commit

Permalink
feat(http): introduce functional interceptors (#47502)
Browse files Browse the repository at this point in the history
This commit introduces a new feature for `provideHttpClient` called
`withInterceptors`. This feature exposes and configures the new concept of
functional interceptors.

Functional interceptors use functions instead of classes to implement an
HTTP interceptor. Such interceptor functions have access to the DI context
from the `EnvironmentInjector` in which they're configured via the
`inject()` function. Otherwise, functional interceptors are identical in
capability to the existing interceptor system.

PR Close #47502
  • Loading branch information
alxhub authored and thePunderWoman committed Oct 6, 2022
1 parent fc69c80 commit 62c7a7a
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 9 deletions.
19 changes: 15 additions & 4 deletions goldens/public-api/common/http/index.md
Expand Up @@ -1738,13 +1738,15 @@ export interface HttpFeature<KindT extends HttpFeatureKind> {
// @public (undocumented)
export enum HttpFeatureKind {
// (undocumented)
CustomXsrfConfiguration = 1,
CustomXsrfConfiguration = 2,
// (undocumented)
JsonpSupport = 3,
Interceptors = 0,
// (undocumented)
LegacyInterceptors = 0,
JsonpSupport = 4,
// (undocumented)
NoXsrfProtection = 2
LegacyInterceptors = 1,
// (undocumented)
NoXsrfProtection = 3
}

// @public
Expand All @@ -1753,6 +1755,9 @@ export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}

// @public
export type HttpHandlerFn = (req: HttpRequest<unknown>) => Observable<HttpEvent<unknown>>;

// @public
export class HttpHeaderResponse extends HttpResponseBase {
constructor(init?: {
Expand Down Expand Up @@ -1790,6 +1795,9 @@ export interface HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>;
}

// @public
export type HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => Observable<HttpEvent<unknown>>;

// @public
export interface HttpParameterCodec {
// (undocumented)
Expand Down Expand Up @@ -2154,6 +2162,9 @@ export class JsonpInterceptor {
// @public (undocumented)
export function provideHttpClient(...features: HttpFeature<HttpFeatureKind>[]): Provider[];

// @public (undocumented)
export function withInterceptors(interceptorFns: HttpInterceptorFn[]): HttpFeature<HttpFeatureKind.Interceptors>;

// @public (undocumented)
export function withJsonpSupport(): HttpFeature<HttpFeatureKind.JsonpSupport>;

Expand Down
4 changes: 2 additions & 2 deletions packages/common/http/public_api.ts
Expand Up @@ -31,11 +31,11 @@ export {HttpBackend, HttpHandler} from './src/backend';
export {HttpClient} from './src/client';
export {HttpContext, HttpContextToken} from './src/context';
export {HttpHeaders} from './src/headers';
export {HTTP_INTERCEPTORS, HttpInterceptor, HttpInterceptorHandler as ɵHttpInterceptorHandler} from './src/interceptor';
export {HTTP_INTERCEPTORS, HttpInterceptor, HttpInterceptorHandler as ɵHttpInterceptorHandler, HttpInterceptorFn, HttpHandlerFn} from './src/interceptor';
export {JsonpClientBackend, JsonpInterceptor} from './src/jsonp';
export {HttpClientJsonpModule, HttpClientModule, HttpClientXsrfModule} from './src/module';
export {HttpParameterCodec, HttpParams, HttpParamsOptions, HttpUrlEncodingCodec} from './src/params';
export {HttpFeature, HttpFeatureKind, provideHttpClient, withJsonpSupport, withLegacyInterceptors, withNoXsrfProtection, withXsrfConfiguration} from './src/provider';
export {HttpFeature, HttpFeatureKind, provideHttpClient, withJsonpSupport, withLegacyInterceptors, withNoXsrfProtection, withXsrfConfiguration, withInterceptors} from './src/provider';
export {HttpRequest} from './src/request';
export {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpResponseBase, HttpSentEvent, HttpStatusCode, HttpUploadProgressEvent, HttpUserEvent} from './src/response';
export {HttpXhrBackend} from './src/xhr';
Expand Down
14 changes: 13 additions & 1 deletion packages/common/http/src/provider.ts
Expand Up @@ -11,11 +11,12 @@ import {InjectionToken, Provider} from '@angular/core';
import {HttpBackend, HttpHandler} from './backend';
import {HttpClient} from './client';
import {HTTP_INTERCEPTOR_FNS, HttpInterceptorFn, HttpInterceptorHandler, legacyInterceptorFnFactory} from './interceptor';
import {JsonpCallbackContext, jsonpCallbackContext, JsonpClientBackend, jsonpInterceptorFn} from './jsonp';
import {jsonpCallbackContext, JsonpCallbackContext, JsonpClientBackend, jsonpInterceptorFn} from './jsonp';
import {HttpXhrBackend} from './xhr';
import {HttpXsrfCookieExtractor, HttpXsrfTokenExtractor, XSRF_COOKIE_NAME, XSRF_ENABLED, XSRF_HEADER_NAME, xsrfInterceptorFn} from './xsrf';

export enum HttpFeatureKind {
Interceptors,
LegacyInterceptors,
CustomXsrfConfiguration,
NoXsrfProtection,
Expand Down Expand Up @@ -70,6 +71,17 @@ export function provideHttpClient(...features: HttpFeature<HttpFeatureKind>[]):
return providers;
}

export function withInterceptors(interceptorFns: HttpInterceptorFn[]):
HttpFeature<HttpFeatureKind.Interceptors> {
return makeHttpFeature(HttpFeatureKind.Interceptors, interceptorFns.map(interceptorFn => {
return {
provide: HTTP_INTERCEPTOR_FNS,
useValue: interceptorFn,
multi: true,
};
}));
}

const LEGACY_INTERCEPTOR_FN = new InjectionToken<HttpInterceptorFn>('LEGACY_INTERCEPTOR_FN');

export function withLegacyInterceptors(): HttpFeature<HttpFeatureKind.LegacyInterceptors> {
Expand Down
113 changes: 111 additions & 2 deletions packages/common/http/test/provider_spec.ts
Expand Up @@ -9,11 +9,12 @@
import {DOCUMENT} from '@angular/common';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, JsonpClientBackend} from '@angular/common/http';
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {InjectionToken, PLATFORM_ID, Provider} from '@angular/core';
import {inject, InjectionToken, PLATFORM_ID, Provider} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {EMPTY, Observable} from 'rxjs';

import {provideHttpClient, withJsonpSupport, withLegacyInterceptors, withNoXsrfProtection, withXsrfConfiguration} from '../src/provider';
import {HttpInterceptorFn} from '../src/interceptor';
import {provideHttpClient, withInterceptors, withJsonpSupport, withLegacyInterceptors, withNoXsrfProtection, withXsrfConfiguration} from '../src/provider';

describe('provideHttp', () => {
beforeEach(() => {
Expand Down Expand Up @@ -77,6 +78,106 @@ describe('provideHttp', () => {
req.flush('');
});

describe('interceptor functions', () => {
it('should allow configuring interceptors', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([
makeLiteralTagInterceptorFn('alpha'),
makeLiteralTagInterceptorFn('beta'),
])),
provideHttpClientTesting(),
],
});

TestBed.inject(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('alpha,beta');
req.flush('');
});

it('should accept multiple separate interceptor configs', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(
withInterceptors([
makeLiteralTagInterceptorFn('alpha'),
]),
withInterceptors([
makeLiteralTagInterceptorFn('beta'),
])),
provideHttpClientTesting(),
],
});

TestBed.inject(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('alpha,beta');
req.flush('');
});

it('should allow injection from an interceptor context', () => {
const ALPHA =
new InjectionToken<string>('alpha', {providedIn: 'root', factory: () => 'alpha'});
const BETA = new InjectionToken<string>('beta', {providedIn: 'root', factory: () => 'beta'});

TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([
makeTokenTagInterceptorFn(ALPHA),
makeTokenTagInterceptorFn(BETA),
])),
provideHttpClientTesting(),
],
});

TestBed.inject(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('alpha,beta');
req.flush('');
});

it('should allow combination with legacy interceptors, before the legacy stack', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(
withInterceptors([
makeLiteralTagInterceptorFn('functional'),
]),
withLegacyInterceptors(),
),
provideHttpClientTesting(),
provideLegacyInterceptor('legacy'),
],
});

TestBed.inject(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('functional,legacy');
req.flush('');
});

it('should allow combination with legacy interceptors, after the legacy stack', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(
withLegacyInterceptors(),
withInterceptors([
makeLiteralTagInterceptorFn('functional'),
]),
),
provideHttpClientTesting(),
provideLegacyInterceptor('legacy'),
],
});

TestBed.inject(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('legacy,functional');
req.flush('');
});
});

describe('xsrf protection', () => {
it('should enable xsrf protection by default', () => {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -224,6 +325,14 @@ function provideLegacyInterceptor(tag: string): Provider {
};
}

function makeLiteralTagInterceptorFn(tag: string): HttpInterceptorFn {
return (req, next) => next(addTagToRequest(req, tag));
}

function makeTokenTagInterceptorFn(tag: InjectionToken<string>): HttpInterceptorFn {
return (req, next) => next(addTagToRequest(req, inject(tag)));
}

function addTagToRequest(req: HttpRequest<unknown>, tag: string): HttpRequest<unknown> {
const prevTagHeader = req.headers.get('X-Tag') ?? '';
const tagHeader = (prevTagHeader.length > 0) ? prevTagHeader + ',' + tag : tag;
Expand Down

0 comments on commit 62c7a7a

Please sign in to comment.