From 5a936b7129ad354f25e0b0ac6ad00e08f9532340 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:08:18 -0500 Subject: [PATCH] fix(http): cancel oversized fetch response bodies Cancel the unread response body before reporting NG02825 when its declared Content-Length exceeds the configured buffer limit. Without cancellation, SSR can finish while the underlying connection remains open. Add regression coverage for the declared-length rejection path. --- packages/common/http/src/fetch.ts | 1 + packages/common/http/test/fetch_spec.ts | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/common/http/src/fetch.ts b/packages/common/http/src/fetch.ts index 81632ccc79c0..e4c263529c7a 100644 --- a/packages/common/http/src/fetch.ts +++ b/packages/common/http/src/fetch.ts @@ -191,6 +191,7 @@ export class FetchBackend implements HttpBackend { Number.isFinite(contentLengthValue) && contentLengthValue > this.maxResponseSize ) { + await response.body.cancel(); throwBodyTooLargeError(this.maxResponseSize); } diff --git a/packages/common/http/test/fetch_spec.ts b/packages/common/http/test/fetch_spec.ts index 72a427791de0..32e4484959f3 100644 --- a/packages/common/http/test/fetch_spec.ts +++ b/packages/common/http/test/fetch_spec.ts @@ -708,6 +708,19 @@ describe('FetchBackend', () => { expect(infiniteStreamFactory.cancelCount).toBe(1); }); + it('cancels an unread response stream when the declared size exceeds the limit', async () => { + infiniteStreamFactory.declaredContentLength = 2049; + + const req = new HttpRequest('GET', '/test', {responseType: 'text'}); + const events = await trackEvents(backend.handle(req)); + const error = events[1] as HttpErrorResponse; + + expect(events.length).toBe(2); + expect(error instanceof HttpErrorResponse).toBeTrue(); + expect(error.error.code).toBe(RuntimeErrorCode.FETCH_RESPONSE_BODY_TOO_LARGE); + expect(infiniteStreamFactory.cancelCount).toBe(1); + }); + it('allows disabling the size limit via dependency injection', async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ @@ -854,6 +867,7 @@ class MockFetchRequest { class InfiniteStreamFetchFactory extends FetchFactory { public cancelCount = 0; + public declaredContentLength?: number; override fetch = async (_input: RequestInfo | URL, _init?: RequestInit): Promise => { const stream = new ReadableStream({ @@ -868,7 +882,12 @@ class InfiniteStreamFetchFactory extends FetchFactory { return new Response(stream, { status: HttpStatusCode.Ok, statusText: 'OK', - headers: {'Content-Type': 'text/plain'}, + headers: { + 'Content-Type': 'text/plain', + ...(this.declaredContentLength === undefined + ? {} + : {'Content-Length': `${this.declaredContentLength}`}), + }, }); }; } @@ -885,7 +904,7 @@ class FiniteChunkFetchFactory extends FetchFactory { return new Response(stream, { status: HttpStatusCode.Ok, statusText: 'OK', - headers: {'Content-Type': 'text/plain'}, + headers: {'Content-Type': 'text/plain', 'Content-Length': '2'}, }); }; }