From af7881abac268d2a6d93bbc2b64fa4a33ce3de31 Mon Sep 17 00:00:00 2001 From: arturovt Date: Tue, 20 May 2025 19:56:29 +0300 Subject: [PATCH] fix(common): cancel reader when app is destroyed (#61528) Streams left in a pending state (due to `break` without cancel) may continue consuming or holding onto data behind the scenes. Calling `reader.cancel()` allows the browser or the underlying system to release any network or memory resources associated with the stream. PR Close #61528 --- packages/common/http/src/fetch.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/common/http/src/fetch.ts b/packages/common/http/src/fetch.ts index 3a45d8f18b9..379a8de7954 100644 --- a/packages/common/http/src/fetch.ts +++ b/packages/common/http/src/fetch.ts @@ -147,6 +147,8 @@ export class FetchBackend implements HttpBackend { // when the zone is nooped. const reqZone = typeof Zone !== 'undefined' && Zone.current; + let canceled = false; + // Perform response processing outside of Angular zone to // ensure no excessive change detection runs are executed // Here calling the async ReadableStreamDefaultReader.read() is responsible for triggering CD @@ -157,6 +159,12 @@ export class FetchBackend implements HttpBackend { // This may happen if the app was explicitly destroyed before // the response returned entirely. if (this.appRef.destroyed) { + // Streams left in a pending state (due to `break` without cancel) may + // continue consuming or holding onto data behind the scenes. + // Calling `reader.cancel()` allows the browser or the underlying + // system to release any network or memory resources associated with the stream. + await reader.cancel(); + canceled = true; break; } @@ -188,6 +196,15 @@ export class FetchBackend implements HttpBackend { } }); + // We need to manage the canceled state — because the Streams API does not + // expose a direct `.state` property on the reader. + // We need to `return` because `parseBody` may not be able to parse chunks + // that were only partially read (due to cancellation caused by app destruction). + if (canceled) { + observer.complete(); + return; + } + // Combine all chunks. const chunksAll = this.concatChunks(chunks, receivedLength); try {