| 72 | */ |
| 73 | @Service() |
| 74 | export class FetchBackend implements HttpBackend { |
| 75 | // We use an arrow function to always reference the current global implementation of `fetch`. |
| 76 | // This is helpful for cases when the global `fetch` implementation is modified by external code, |
| 77 | // see https://github.com/angular/angular/issues/57527. |
| 78 | private readonly fetchImpl = |
| 79 | inject(FetchFactory, {optional: true})?.fetch ?? ((...args) => globalThis.fetch(...args)); |
| 80 | private readonly ngZone = inject(NgZone); |
| 81 | private readonly destroyRef = inject(DestroyRef); |
| 82 | private readonly maxResponseSize = inject(HTTP_FETCH_MAX_RESPONSE_SIZE); |
| 83 | |
| 84 | handle(request: HttpRequest<any>): Observable<HttpEvent<any>> { |
| 85 | return new Observable((observer) => { |
| 86 | const aborter = new AbortController(); |
| 87 | |
| 88 | this.doRequest(request, aborter.signal, observer).then(noop, (error) => |
| 89 | observer.error(new HttpErrorResponse({error})), |
| 90 | ); |
| 91 | |
| 92 | let timeoutId: ReturnType<typeof setTimeout> | undefined; |
| 93 | if (request.timeout) { |
| 94 | // TODO: Replace with AbortSignal.any([aborter.signal, AbortSignal.timeout(request.timeout)]) |
| 95 | // when AbortSignal.any support is Baseline widely available (NET nov. 2026) |
| 96 | timeoutId = this.ngZone.runOutsideAngular(() => |
| 97 | setTimeout(() => { |
| 98 | if (!aborter.signal.aborted) { |
| 99 | aborter.abort(new DOMException('signal timed out', 'TimeoutError')); |
| 100 | } |
| 101 | }, request.timeout), |
| 102 | ); |
| 103 | } |
| 104 | |
| 105 | return () => { |
| 106 | if (timeoutId !== undefined) { |
| 107 | clearTimeout(timeoutId); |
| 108 | } |
| 109 | aborter.abort(); |
| 110 | }; |
| 111 | }); |
| 112 | } |
| 113 | |
| 114 | private async doRequest( |
| 115 | request: HttpRequest<any>, |
| 116 | signal: AbortSignal, |
| 117 | observer: Observer<HttpEvent<any>>, |
| 118 | ): Promise<void> { |
| 119 | const init = this.createRequestInit(request); |
| 120 | let response; |
| 121 | try { |
| 122 | // Run fetch outside of Angular zone. |
| 123 | // This is due to Node.js fetch implementation (Undici) which uses a number of setTimeouts to check if |
| 124 | // the response should eventually timeout which causes extra CD cycles every 500ms |
| 125 | const fetchPromise = this.ngZone.runOutsideAngular(() => |
| 126 | this.fetchImpl(request.urlWithParams, {signal, ...init}), |
| 127 | ); |
| 128 | |
| 129 | // Make sure Zone.js doesn't trigger false-positive unhandled promise |
| 130 | // error in case the Promise is rejected synchronously. See function |
| 131 | // description for additional information. |