Skip to content

Commit

Permalink
feat(http): allow for child HttpClients to request via parents (#47502
Browse files Browse the repository at this point in the history
)

Ordinarily, providing `HttpClient` (either via `provideHttpClient` or the
`HttpClientModule`) creates an entirely separate HTTP context. Requests made
via that client are not passed through the interceptor chains that are
configured in a parent injector, for this example.

This commit introduces a new option for `provideHttpClient` called
`withRequestsMadeViaParent()`. When this option is passed, requests made in
the child context flow through any injectors, etc. and are then handed off
to the parent context.

This addresses a longstanding issue with interceptors where it's not
possible to extend the set of interceptors in a child context without
repeating all of the interceptors from the parent.

PR Close #47502
  • Loading branch information
alxhub authored and thePunderWoman committed Oct 6, 2022
1 parent 62c7a7a commit 3ba99e2
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 7 deletions.
7 changes: 6 additions & 1 deletion goldens/public-api/common/http/index.md
Expand Up @@ -1746,7 +1746,9 @@ export enum HttpFeatureKind {
// (undocumented)
LegacyInterceptors = 1,
// (undocumented)
NoXsrfProtection = 3
NoXsrfProtection = 3,
// (undocumented)
RequestsMadeViaParent = 5
}

// @public
Expand Down Expand Up @@ -2174,6 +2176,9 @@ export function withLegacyInterceptors(): HttpFeature<HttpFeatureKind.LegacyInte
// @public (undocumented)
export function withNoXsrfProtection(): HttpFeature<HttpFeatureKind.NoXsrfProtection>;

// @public (undocumented)
export function withRequestsMadeViaParent(): HttpFeature<HttpFeatureKind.RequestsMadeViaParent>;

// @public (undocumented)
export function withXsrfConfiguration({ cookieName, headerName }: {
cookieName?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/common/http/public_api.ts
Expand Up @@ -35,7 +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, withInterceptors} from './src/provider';
export {HttpFeature, HttpFeatureKind, provideHttpClient, withJsonpSupport, withLegacyInterceptors, withNoXsrfProtection, withXsrfConfiguration, withInterceptors, withRequestsMadeViaParent} 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
22 changes: 21 additions & 1 deletion packages/common/http/src/provider.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken, Provider} from '@angular/core';
import {inject, InjectionToken, Provider} from '@angular/core';

import {HttpBackend, HttpHandler} from './backend';
import {HttpClient} from './client';
Expand All @@ -21,6 +21,7 @@ export enum HttpFeatureKind {
CustomXsrfConfiguration,
NoXsrfProtection,
JsonpSupport,
RequestsMadeViaParent,
}

export interface HttpFeature<KindT extends HttpFeatureKind> {
Expand Down Expand Up @@ -133,3 +134,22 @@ export function withJsonpSupport(): HttpFeature<HttpFeatureKind.JsonpSupport> {
{provide: HTTP_INTERCEPTOR_FNS, useValue: jsonpInterceptorFn, multi: true},
]);
}

/**
* @developerPreview
*/
export function withRequestsMadeViaParent(): HttpFeature<HttpFeatureKind.RequestsMadeViaParent> {
return makeHttpFeature(HttpFeatureKind.RequestsMadeViaParent, [
{
provide: HttpBackend,
useFactory: () => {
const handlerFromParent = inject(HttpHandler, {skipSelf: true, optional: true});
if (ngDevMode && handlerFromParent === null) {
throw new Error(
'withRequestsMadeViaParent() can only be used when the parent injector also configures HttpClient');
}
return handlerFromParent;
},
},
]);
}
102 changes: 98 additions & 4 deletions packages/common/http/test/provider_spec.ts
Expand Up @@ -6,15 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/

import {DOCUMENT} from '@angular/common';
import {DOCUMENT, XhrFactory} 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 {inject, InjectionToken, PLATFORM_ID, Provider} from '@angular/core';
import {HttpClientTestingModule, HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {createEnvironmentInjector, EnvironmentInjector, inject, InjectionToken, PLATFORM_ID, Provider} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {EMPTY, Observable} from 'rxjs';

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

describe('provideHttp', () => {
beforeEach(() => {
Expand Down Expand Up @@ -272,6 +272,100 @@ describe('provideHttp', () => {
});
});

describe('withRequestsMadeViaParent()', () => {
it('should have independent HTTP setups if not explicitly specified', async () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});

const child = createEnvironmentInjector(
[
provideHttpClient(), {
provide: XhrFactory,
useValue: {
build: () => {
throw new Error('Request reached the "backend".');
},
},
}
],
TestBed.inject(EnvironmentInjector));

// Because `child` is an entirely independent HTTP context, it is not connected to the
// HTTP testing backend from the parent injector, and requests attempted via the child's
// `HttpClient` will fail.
await expectAsync(child.get(HttpClient).get('/test').toPromise()).toBeRejected();
});

it('should connect child to parent configuration if specified', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});

const child = createEnvironmentInjector(
[provideHttpClient(withRequestsMadeViaParent())], TestBed.inject(EnvironmentInjector));


// `child` is now to the parent HTTP context and therefore the testing backend, and so a
// request made via its `HttpClient` can be made.
child.get(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
req.flush('');
});

it('should include interceptors from both parent and child contexts', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([makeLiteralTagInterceptorFn('parent')])),
provideHttpClientTesting(),
],
});

const child = createEnvironmentInjector(
[provideHttpClient(
withRequestsMadeViaParent(),
withInterceptors([makeLiteralTagInterceptorFn('child')]),
)],
TestBed.inject(EnvironmentInjector));


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

it('should be able to connect to a legacy-provided HttpClient context', () => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
],
providers: [
provideLegacyInterceptor('parent'),
],
});

const child = createEnvironmentInjector(
[provideHttpClient(
withRequestsMadeViaParent(),
withInterceptors([makeLiteralTagInterceptorFn('child')]),
)],
TestBed.inject(EnvironmentInjector));


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

describe('compatibility with Http NgModules', () => {
it('should function when configuring HTTP both ways in the same injector', () => {
TestBed.configureTestingModule({
Expand Down

0 comments on commit 3ba99e2

Please sign in to comment.