Skip to content

Commit

Permalink
feat(http): introduce the provideHttpClient() API (#47502)
Browse files Browse the repository at this point in the history
This commit introduces the main components of the `provideHttpClient()`
provider API, designed in the style of `provideRouter()`. Initial features
are defined for including legacy class-based interceptors, JSONP support,
and configuring or disabling the builtin XSRF protection.

This API is an alternative to providing `HttpClient` via the
`HttpClientModule`, and is more tree-shakable and more capable than the
NgModule implementation.

Tests are included to validate the new configuration format as well as the
interoperability of the two styles of providing and configuring
`HttpClient`.

PR Close #47502
  • Loading branch information
alxhub authored and thePunderWoman committed Oct 6, 2022
1 parent 84d0d33 commit e47b129
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 1 deletion.
39 changes: 39 additions & 0 deletions goldens/public-api/common/http/index.md
Expand Up @@ -9,6 +9,7 @@ import * as i0 from '@angular/core';
import { InjectionToken } from '@angular/core';
import { ModuleWithProviders } from '@angular/core';
import { Observable } from 'rxjs';
import { Provider } from '@angular/core';
import { XhrFactory as XhrFactory_2 } from '@angular/common';

// @public
Expand Down Expand Up @@ -1726,6 +1727,26 @@ export enum HttpEventType {
User = 5
}

// @public (undocumented)
export interface HttpFeature<KindT extends HttpFeatureKind> {
// (undocumented)
ɵkind: KindT;
// (undocumented)
ɵproviders: Provider[];
}

// @public (undocumented)
export enum HttpFeatureKind {
// (undocumented)
CustomXsrfConfiguration = 1,
// (undocumented)
JsonpSupport = 3,
// (undocumented)
LegacyInterceptors = 0,
// (undocumented)
NoXsrfProtection = 2
}

// @public
export abstract class HttpHandler {
// (undocumented)
Expand Down Expand Up @@ -2130,6 +2151,24 @@ export class JsonpInterceptor {
static ɵprov: i0.ɵɵInjectableDeclaration<JsonpInterceptor>;
}

// @public (undocumented)
export function provideHttpClient(...features: HttpFeature<HttpFeatureKind>[]): Provider[];

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

// @public (undocumented)
export function withLegacyInterceptors(): HttpFeature<HttpFeatureKind.LegacyInterceptors>;

// @public (undocumented)
export function withNoXsrfProtection(): HttpFeature<HttpFeatureKind.NoXsrfProtection>;

// @public (undocumented)
export function withXsrfConfiguration({ cookieName, headerName }: {
cookieName?: string;
headerName?: string;
}): HttpFeature<HttpFeatureKind.CustomXsrfConfiguration>;

// @public @deprecated
export type XhrFactory = XhrFactory_2;

Expand Down
1 change: 1 addition & 0 deletions packages/common/http/public_api.ts
Expand Up @@ -35,6 +35,7 @@ export {HTTP_INTERCEPTORS, HttpInterceptor, HttpInterceptorHandler as ɵHttpInte
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 {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
3 changes: 2 additions & 1 deletion packages/common/http/src/module.ts
Expand Up @@ -97,8 +97,9 @@ export class HttpClientXsrfModule {
*/
providers: [
HttpClient,
{provide: HttpHandler, useClass: HttpInterceptorHandler},
HttpXhrBackend,
HttpInterceptorHandler,
{provide: HttpHandler, useExisting: HttpInterceptorHandler},
{provide: HttpBackend, useExisting: HttpXhrBackend},
{provide: LEGACY_INTERCEPTOR_FN, useFactory: legacyInterceptorFnFactory},
{
Expand Down
121 changes: 121 additions & 0 deletions packages/common/http/src/provider.ts
@@ -0,0 +1,121 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* 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 {Provider} from '@angular/core';

import {HttpBackend, HttpHandler} from './backend';
import {HttpClient} from './client';
import {HTTP_INTERCEPTOR_FNS, HttpInterceptorHandler, LEGACY_INTERCEPTOR_FN, legacyInterceptorFnFactory} from './interceptor';
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 {
LegacyInterceptors,
CustomXsrfConfiguration,
NoXsrfProtection,
JsonpSupport,
}

export interface HttpFeature<KindT extends HttpFeatureKind> {
ɵkind: KindT;
ɵproviders: Provider[];
}

function makeHttpFeature<KindT extends HttpFeatureKind>(
kind: KindT, providers: Provider[]): HttpFeature<KindT> {
return {
ɵkind: kind,
ɵproviders: providers,
};
}


export function provideHttpClient(...features: HttpFeature<HttpFeatureKind>[]): Provider[] {
if (ngDevMode) {
const featureKinds = new Set(features.map(f => f.ɵkind));
if (featureKinds.has(HttpFeatureKind.NoXsrfProtection) &&
featureKinds.has(HttpFeatureKind.CustomXsrfConfiguration)) {
throw new Error(
ngDevMode ?
`Configuration error: found both withXsrfConfiguration() and withNoXsrfProtection() in the same call to provideHttpClient(), which is a contradiction.` :
'');
}
}

const providers: Provider[] = [
HttpClient,
HttpXhrBackend,
HttpInterceptorHandler,
{provide: HttpHandler, useExisting: HttpInterceptorHandler},
{provide: HttpBackend, useExisting: HttpXhrBackend},
{
provide: HTTP_INTERCEPTOR_FNS,
useValue: xsrfInterceptorFn,
multi: true,
},
{provide: XSRF_ENABLED, useValue: true},
{provide: HttpXsrfTokenExtractor, useClass: HttpXsrfCookieExtractor},
];

for (const feature of features) {
providers.push(...feature.ɵproviders);
}

return providers;
}

export function withLegacyInterceptors(): HttpFeature<HttpFeatureKind.LegacyInterceptors> {
// Note: the legacy interceptor function is provided here via an intermediate token
// (`LEGACY_INTERCEPTOR_FN`), using a pattern which guarantees that if these providers are
// included multiple times, all of the multi-provider entries will have the same instance of the
// interceptor function. That way, the `HttpINterceptorHandler` will dedup them and legacy
// interceptors will not run multiple times.
return makeHttpFeature(HttpFeatureKind.LegacyInterceptors, [
{
provide: LEGACY_INTERCEPTOR_FN,
useFactory: legacyInterceptorFnFactory,
},
{
provide: HTTP_INTERCEPTOR_FNS,
useExisting: LEGACY_INTERCEPTOR_FN,
multi: true,
}
]);
}

export function withXsrfConfiguration(
{cookieName, headerName}: {cookieName?: string, headerName?: string}):
HttpFeature<HttpFeatureKind.CustomXsrfConfiguration> {
const providers: Provider[] = [];
if (cookieName !== undefined) {
providers.push({provide: XSRF_COOKIE_NAME, useValue: cookieName});
}
if (headerName !== undefined) {
providers.push({provide: XSRF_HEADER_NAME, useValue: headerName});
}

return makeHttpFeature(HttpFeatureKind.CustomXsrfConfiguration, providers);
}

export function withNoXsrfProtection(): HttpFeature<HttpFeatureKind.NoXsrfProtection> {
return makeHttpFeature(HttpFeatureKind.NoXsrfProtection, [
{
provide: XSRF_ENABLED,
useValue: false,
},
]);
}

export function withJsonpSupport(): HttpFeature<HttpFeatureKind.JsonpSupport> {
return makeHttpFeature(HttpFeatureKind.JsonpSupport, [
JsonpClientBackend,
{provide: JsonpCallbackContext, useFactory: jsonpCallbackContext},
{provide: HTTP_INTERCEPTOR_FNS, useValue: jsonpInterceptorFn, multi: true},
]);
}

0 comments on commit e47b129

Please sign in to comment.