fix(http): Dynamicaly call the global fetch implementation (#57531)

Instead of using the reference that existing when `FetchBackend` is setup.

fixes #57527

PR Close #57531
This commit is contained in:
Matthieu Riegler 2024-08-26 21:53:37 +02:00 committed by Andrew Kushnir
parent fe5c4e086a
commit c2892fee58
2 changed files with 44 additions and 2 deletions

View file

@ -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>> {

View file

@ -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<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 {