From f58c781f01b9cf8acd2cd44865750042f44c8489 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:33:13 -0400 Subject: [PATCH] fix(@angular/build): safeguard Karma builder stream controller against closed state When running Karma tests or when stream consumers (such as Architect test harness `executeOnce`) cancel the builder output stream early, both `ProgressNotifierReporter.onRunComplete` and `karma.Server` exit callbacks can attempt to enqueue results or close the `ReadableStreamController`. Under WHATWG Streams specification rules, calling `.enqueue()` or `.close()` on a controller whose `desiredSize` is `null` (closed or cancelled) throws `TypeError [ERR_INVALID_STATE]: Invalid state: Controller is already closed`. This change checks `controller.desiredSize !== null` and wraps enqueue/close calls in a try-catch block to gracefully handle closed controllers. --- .../src/builders/karma/application_builder.ts | 21 +++++++++++++----- .../src/builders/karma/progress-reporter.ts | 22 ++++++++++++++----- .../karma/tests/behavior/errors_spec.ts | 13 +++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index b9a28e1bf0b0..d0adb6c3cf0e 100644 --- a/packages/angular/build/src/builders/karma/application_builder.ts +++ b/packages/angular/build/src/builders/karma/application_builder.ts @@ -11,7 +11,6 @@ import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from import { randomUUID } from 'node:crypto'; import { rmSync } from 'node:fs'; import * as fs from 'node:fs/promises'; -import { createRequire } from 'node:module'; import path from 'node:path'; import { ReadableStream } from 'node:stream/web'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; @@ -66,8 +65,14 @@ export function execute( init = await initializeApplication(normalizedOptions, context, karmaOptions, transforms); } catch (err) { if (err instanceof ApplicationBuildError) { - controller.enqueue({ success: false, message: err.message }); - controller.close(); + if (controller.desiredSize !== null) { + try { + controller.enqueue({ success: false, message: err.message }); + controller.close(); + } catch { + // Stream controller may already be closed or cancelled + } + } return; } @@ -85,8 +90,14 @@ export function execute( // Close the stream once the Karma server returns. karmaServer = new karma.Server(karmaConfig as Config, (exitCode) => { - controller.enqueue({ success: exitCode === 0 }); - controller.close(); + if (controller.desiredSize !== null) { + try { + controller.enqueue({ success: exitCode === 0 }); + controller.close(); + } catch { + // Stream controller may already be closed or cancelled + } + } }); await karmaServer.start(); diff --git a/packages/angular/build/src/builders/karma/progress-reporter.ts b/packages/angular/build/src/builders/karma/progress-reporter.ts index 16824badd095..6fc92b6c74c8 100644 --- a/packages/angular/build/src/builders/karma/progress-reporter.ts +++ b/packages/angular/build/src/builders/karma/progress-reporter.ts @@ -59,8 +59,18 @@ export function injectKarmaReporter( break; } + if (controller.desiredSize === null) { + break; + } + if (buildOutput.kind === ResultKind.Failure) { - controller.enqueue({ success: false, message: 'Build failed' }); + if (controller.desiredSize !== null) { + try { + controller.enqueue({ success: false, message: 'Build failed' }); + } catch { + // Stream controller may already be closed or cancelled + } + } } else if ( buildOutput.kind === ResultKind.Incremental || buildOutput.kind === ResultKind.Full @@ -81,10 +91,12 @@ export function injectKarmaReporter( } onRunComplete = function (_browsers: unknown, results: RunCompleteInfo): void { - if (results.exitCode === 0) { - controller.enqueue({ success: true }); - } else { - controller.enqueue({ success: false }); + if (controller.desiredSize !== null) { + try { + controller.enqueue({ success: results.exitCode === 0 }); + } catch { + // Stream controller may already be closed or cancelled + } } }; } diff --git a/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts b/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts index 366cc2aa20a1..a69698b8d743 100644 --- a/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts +++ b/packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts @@ -28,5 +28,18 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => { expect(result?.success).toBeFalse(); }); + + it('handles stream cancellation gracefully in watch mode', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + watch: true, + }); + + const { result } = await harness.executeOnce({ + outputLogsOnFailure: false, + }); + + expect(result?.success).toBeTrue(); + }); }); });