Skip to content

Commit d820216

Browse files
committed
fix(@probitas/probitas): prevent BadResource error in subprocess HTTP/2 cleanup
Subprocess execution with HTTP/2 clients was causing "BadResource: Bad resource ID" errors from node:http2 due to premature IPC connection closure during active HTTP/2 stream finalization. The issue occurred because: 1. IpcConnection.close() did not await writer.close(), abandoning pending writes 2. Parent process closed IPC before subprocess completed cleanup 3. Subprocess exited immediately without giving HTTP/2 sessions time to finalize Fixed by: - Making IpcConnection.close() async and awaiting writer.close() - Awaiting closeIpc() calls in subprocess templates - Reordering parent cleanup to wait for subprocess exit before closing IPC - Adding 100ms grace period before subprocess exit for resource cleanup This ensures HTTP/2 sessions can properly close their streams before the underlying TCP connection is terminated.
1 parent 16dfd14 commit d820216

5 files changed

Lines changed: 25 additions & 15 deletions

File tree

src/cli/_templates/list.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ async function main(): Promise<void> {
7777
console.error("Subprocess error:", error);
7878
}
7979
} finally {
80-
closeIpc(ipc);
80+
// Await close to ensure all pending writes are flushed
81+
await closeIpc(ipc);
8182
}
8283
}
8384

8485
// Run main and exit explicitly to avoid async operations keeping process alive
8586
main().finally(() => {
86-
// Ensure process exits after output is flushed
87-
setTimeout(() => Deno.exit(0), 0);
87+
// Give resources time to clean up properly
88+
setTimeout(() => Deno.exit(0), 100);
8889
});

src/cli/_templates/run.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,15 @@ async function main(): Promise<void> {
149149
console.error("Subprocess error:", error);
150150
}
151151
} finally {
152-
closeIpc(ipc);
152+
// Await close to ensure all pending writes are flushed
153+
await closeIpc(ipc);
153154
}
154155
}
155156

156157
// Run main and exit explicitly to avoid LogTape keeping process alive
157158
main().finally(() => {
158-
// Ensure process exits after output is flushed
159-
setTimeout(() => Deno.exit(0), 0);
159+
// Give HTTP/2 and other resources time to clean up properly.
160+
// This prevents "BadResource: Bad resource ID" errors from node:http2
161+
// that occur when connections are closed while HTTP/2 streams are active.
162+
setTimeout(() => Deno.exit(0), 100);
160163
});

src/cli/_templates/utils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export interface IpcConnection {
3030
writer: WritableStreamDefaultWriter<Uint8Array>;
3131
/** Text encoder for converting strings to bytes */
3232
encoder: TextEncoder;
33-
/** Close the underlying TCP connection */
34-
close: () => void;
33+
/** Close the underlying TCP connection (async to properly flush writes) */
34+
close: () => Promise<void>;
3535
}
3636

3737
/**
@@ -77,11 +77,12 @@ export async function connectIpc(port: number): Promise<IpcConnection> {
7777
readable,
7878
writer,
7979
encoder: new TextEncoder(),
80-
close: () => {
80+
close: async () => {
8181
try {
82-
writer.close();
82+
// Await writer.close() to ensure all pending writes are flushed
83+
await writer.close();
8384
} catch {
84-
// Already closed
85+
// Already closed or errored
8586
}
8687
try {
8788
conn.close();
@@ -136,6 +137,6 @@ export async function writeOutput(
136137
*
137138
* @param ipc - IPC connection to close
138139
*/
139-
export function closeIpc(ipc: IpcConnection): void {
140-
ipc.close();
140+
export async function closeIpc(ipc: IpcConnection): Promise<void> {
141+
await ipc.close();
141142
}

src/cli/commands/list.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,11 @@ async function runListSubprocess(
272272

273273
throw new Error("Subprocess ended without sending result");
274274
} finally {
275+
// Wait for subprocess to exit first to allow proper cleanup.
276+
// This prevents closing IPC while the subprocess is still writing.
277+
await proc.status;
275278
ipc.close();
276279
listener.close();
277-
await proc.status;
278280
// Clean up temporary directory
279281
await Deno.remove(tempDir, { recursive: true }).catch(() => {});
280282
}

src/cli/commands/run.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,12 @@ async function runWithSubprocess(
334334

335335
throw new Error("Subprocess ended without sending result");
336336
} finally {
337+
// Wait for subprocess to exit first to allow proper cleanup.
338+
// This prevents closing IPC while the subprocess is still writing,
339+
// which could cause "BadResource: Bad resource ID" errors.
340+
await proc.status;
337341
ipc.close();
338342
listener.close();
339-
await proc.status;
340343
// Clean up temporary directory
341344
await Deno.remove(tempDir, { recursive: true }).catch(() => {});
342345
}

0 commit comments

Comments
 (0)