Skip to content

Commit

Permalink
fix(http): Dynamicaly call the global fetch implementation (#57531)
Browse files Browse the repository at this point in the history
Instead of using the reference that existing when `FetchBackend` is setup.

fixes #57527

PR Close #57531
  • Loading branch information
JeanMeche authored and AndrewKushnir committed Sep 3, 2024
1 parent 2324d9b commit de68e04
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 2 deletions.
6 changes: 4 additions & 2 deletions packages/common/http/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>): Observable<HttpEvent<any>> {
Expand Down
40 changes: 40 additions & 0 deletions packages/common/http/test/fetch_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<unknown>('').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<unknown>('').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 {
Expand Down

0 comments on commit de68e04

Please sign in to comment.