diff --git a/packages/common/http/src/fetch.ts b/packages/common/http/src/fetch.ts index 876df4b0c85..65b43e9e573 100644 --- a/packages/common/http/src/fetch.ts +++ b/packages/common/http/src/fetch.ts @@ -52,9 +52,11 @@ function getResponseUrl(response: Response): string | null { */ @Injectable() export class FetchBackend implements HttpBackend { - // We need to bind the native fetch to its context or it will throw an "illegal invocation" + // We use an arrow function to always reference the current global implementation of `fetch`. + // This is helpful for cases when the global `fetch` implementation is modified by external code, + // see https://github.com/angular/angular/issues/57527. private readonly fetchImpl = - inject(FetchFactory, {optional: true})?.fetch ?? fetch.bind(globalThis); + inject(FetchFactory, {optional: true})?.fetch ?? ((...args) => globalThis.fetch(...args)); private readonly ngZone = inject(NgZone); handle(request: HttpRequest): Observable> { diff --git a/packages/common/http/test/fetch_spec.ts b/packages/common/http/test/fetch_spec.ts index cb20ee7c7bf..628640a5025 100644 --- a/packages/common/http/test/fetch_spec.ts +++ b/packages/common/http/test/fetch_spec.ts @@ -12,11 +12,14 @@ import {Observable, of, Subject} from 'rxjs'; import {catchError, retry, scan, skip, take, toArray} from 'rxjs/operators'; import { + HttpClient, HttpDownloadProgressEvent, HttpErrorResponse, HttpHeaderResponse, HttpParams, HttpStatusCode, + provideHttpClient, + withFetch, } from '../public_api'; import {FetchBackend, FetchFactory} from '../src/fetch'; @@ -416,6 +419,43 @@ describe('FetchBackend', async () => { fetchMock.mockFlush(0, 'CORS 0 status'); }); }); + + describe('dynamic global fetch', () => { + beforeEach(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch())], + }); + }); + + it('should use the current implementation of the global fetch', async () => { + const originalFetch = globalThis.fetch; + + try { + const fakeFetch = jasmine + .createSpy('', () => Promise.resolve(new Response(JSON.stringify({foo: 'bar'})))) + .and.callThrough(); + globalThis.fetch = fakeFetch; + + const client = TestBed.inject(HttpClient); + expect(fakeFetch).not.toHaveBeenCalled(); + let response = await client.get('').toPromise(); + expect(fakeFetch).toHaveBeenCalled(); + expect(response).toEqual({foo: 'bar'}); + + // We dynamicaly change the implementation of fetch + const fakeFetch2 = jasmine + .createSpy('', () => Promise.resolve(new Response(JSON.stringify({foo: 'baz'})))) + .and.callThrough(); + globalThis.fetch = fakeFetch2; + response = await client.get('').toPromise(); + expect(response).toEqual({foo: 'baz'}); + } finally { + // We need to restore the original fetch implementation, else the tests might become flaky + globalThis.fetch = originalFetch; + } + }); + }); }); export class MockFetchFactory extends FetchFactory {