Skip to content

Commit 760bfdc

Browse files
AndrewKushniratscott
authored andcommitted
refactor(http): warn when HttpClient doesn't use fetch during SSR (angular#52037)
This commit adds a logic to produce a warning in case HttpClient doesn't use fetch during SSR. It's recommended to use `fetch` for performance and compatibility reasons. PR Close angular#52037
1 parent 05d1fac commit 760bfdc

File tree

4 files changed

+97
-5
lines changed

4 files changed

+97
-5
lines changed

goldens/public-api/common/http/errors.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
// @public
88
export const enum RuntimeErrorCode {
99
// (undocumented)
10-
MISSING_JSONP_MODULE = -2800
10+
MISSING_JSONP_MODULE = -2800,
11+
// (undocumented)
12+
NOT_USING_FETCH_BACKEND_IN_SSR = 2801
1113
}
1214

1315
// (No @packageDocumentation comment for this package)

packages/common/http/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
*/
1313
export const enum RuntimeErrorCode {
1414
MISSING_JSONP_MODULE = -2800,
15+
NOT_USING_FETCH_BACKEND_IN_SSR = 2801,
1516
}

packages/common/http/src/interceptor.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {EnvironmentInjector, inject, Injectable, InjectionToken, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core';
9+
import {isPlatformServer} from '@angular/common';
10+
import {EnvironmentInjector, inject, Injectable, InjectionToken, PLATFORM_ID, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core';
1011
import {Observable} from 'rxjs';
1112
import {finalize} from 'rxjs/operators';
1213

1314
import {HttpBackend, HttpHandler} from './backend';
15+
import {RuntimeErrorCode} from './errors';
16+
import {FetchBackend} from './fetch';
1417
import {HttpRequest} from './request';
1518
import {HttpEvent} from './response';
1619

@@ -220,6 +223,13 @@ export function legacyInterceptorFnFactory(): HttpInterceptorFn {
220223
};
221224
}
222225

226+
let fetchBackendWarningDisplayed = false;
227+
228+
/** Internal function to reset the flag in tests */
229+
export function resetFetchBackendWarningFlag() {
230+
fetchBackendWarningDisplayed = false;
231+
}
232+
223233
@Injectable()
224234
export class HttpInterceptorHandler extends HttpHandler {
225235
private chain: ChainedInterceptorFn<unknown>|null = null;
@@ -233,6 +243,24 @@ export class HttpInterceptorHandler extends HttpHandler {
233243
// is used.
234244
const primaryHttpBackend = inject(PRIMARY_HTTP_BACKEND, {optional: true});
235245
this.backend = primaryHttpBackend ?? backend;
246+
247+
// We strongly recommend using fetch backend for HTTP calls when SSR is used
248+
// for an application. The logic below checks if that's the case and produces
249+
// a warning otherwise.
250+
if ((typeof ngDevMode === 'undefined' || ngDevMode) && !fetchBackendWarningDisplayed) {
251+
const isServer = isPlatformServer(injector.get(PLATFORM_ID));
252+
if (isServer && !(this.backend instanceof FetchBackend)) {
253+
fetchBackendWarningDisplayed = true;
254+
injector.get(Console).warn(formatRuntimeError(
255+
RuntimeErrorCode.NOT_USING_FETCH_BACKEND_IN_SSR,
256+
'Angular detected that `HttpClient` is not configured ' +
257+
'to use `fetch` APIs. It\'s strongly recommended to ' +
258+
'enable `fetch` for applications that use Server-Side Rendering ' +
259+
'for better performance and compatibility. ' +
260+
'To enable `fetch`, add the `withFetch()` to the `provideHttpClient()` ' +
261+
'call at the root of the application.'));
262+
}
263+
}
236264
}
237265

238266
override handle(initialRequest: HttpRequest<any>): Observable<HttpEvent<any>> {

packages/common/http/test/provider_spec.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import {createEnvironmentInjector, EnvironmentInjector, inject, InjectionToken,
1313
import {TestBed} from '@angular/core/testing';
1414
import {EMPTY, Observable} from 'rxjs';
1515

16-
import {HttpInterceptorFn} from '../src/interceptor';
16+
import {HttpInterceptorFn, resetFetchBackendWarningFlag} from '../src/interceptor';
1717
import {provideHttpClient, withFetch, withInterceptors, withInterceptorsFromDi, withJsonpSupport, withNoXsrfProtection, withRequestsMadeViaParent, withXsrfConfiguration} from '../src/provider';
1818

19-
describe('provideHttp', () => {
19+
describe('provideHttpClient', () => {
2020
beforeEach(() => {
2121
setCookie('');
2222
TestBed.resetTestingModule();
@@ -390,10 +390,25 @@ describe('provideHttp', () => {
390390

391391
describe('fetch support', () => {
392392
it('withFetch', () => {
393+
resetFetchBackendWarningFlag();
394+
395+
const consoleWarnSpy = spyOn(console, 'warn');
396+
393397
TestBed.resetTestingModule();
394-
TestBed.configureTestingModule({providers: [provideHttpClient(withFetch())]});
398+
TestBed.configureTestingModule({
399+
providers: [
400+
// Setting this flag to verify that there are no
401+
// `console.warn` produced for cases when `fetch`
402+
// is enabled and we are running in a browser.
403+
{provide: PLATFORM_ID, useValue: 'browser'},
404+
provideHttpClient(withFetch()),
405+
]
406+
});
395407
const fetchBackend = TestBed.inject(HttpBackend);
396408
expect(fetchBackend).toBeInstanceOf(FetchBackend);
409+
410+
// Make sure there are no warnings produced.
411+
expect(consoleWarnSpy.calls.count()).toBe(0);
397412
});
398413

399414
it('withFetch should always override the backend', () => {
@@ -411,6 +426,52 @@ describe('provideHttp', () => {
411426
const handler = TestBed.inject(HttpHandler);
412427
expect((handler as any).backend).toBeInstanceOf(FetchBackend);
413428
});
429+
430+
it('should not warn if fetch is not configured when running in a browser', () => {
431+
resetFetchBackendWarningFlag();
432+
433+
const consoleWarnSpy = spyOn(console, 'warn');
434+
435+
TestBed.resetTestingModule();
436+
TestBed.configureTestingModule({
437+
providers: [
438+
// Setting this flag to verify that there are no
439+
// `console.warn` produced for cases when `fetch`
440+
// is enabled and we are running in a browser.
441+
{provide: PLATFORM_ID, useValue: 'browser'},
442+
provideHttpClient(),
443+
]
444+
});
445+
446+
TestBed.inject(HttpHandler);
447+
448+
// Make sure there are no warnings produced.
449+
expect(consoleWarnSpy.calls.count()).toBe(0);
450+
});
451+
452+
it('should warn during SSR if fetch is not configured', () => {
453+
resetFetchBackendWarningFlag();
454+
455+
const consoleWarnSpy = spyOn(console, 'warn');
456+
457+
TestBed.resetTestingModule();
458+
TestBed.configureTestingModule({
459+
providers: [
460+
// Setting this flag to verify that there is a
461+
// `console.warn` produced in case `fetch` is not
462+
// enabled while running code on the server.
463+
{provide: PLATFORM_ID, useValue: 'server'},
464+
provideHttpClient(),
465+
]
466+
});
467+
468+
TestBed.inject(HttpHandler);
469+
470+
expect(consoleWarnSpy.calls.count()).toBe(1);
471+
expect(consoleWarnSpy.calls.argsFor(0)[0])
472+
.toContain(
473+
'NG02801: Angular detected that `HttpClient` is not configured to use `fetch` APIs.');
474+
});
414475
});
415476
});
416477

0 commit comments

Comments
 (0)