Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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