Skip to content

Commit 4c10f54

Browse files
committed
quic: fix potential crash from unobserved closed
Signed-off-by: Tim Perry <pimterry@gmail.com>
1 parent c612f35 commit 4c10f54

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

lib/internal/quic/quic.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3586,7 +3586,9 @@ class QuicSession {
35863586

35873587
if (error) {
35883588
// If the session is still waiting to be closed, and error
3589-
// is specified, reject the closed promise.
3589+
// is specified, reject the closed promise. Mark it handled first
3590+
// (as with opened) to silence errors if it's not actually awaited.
3591+
markPromiseAsHandled(inner.pendingClose.promise);
35903592
inner.pendingClose.reject?.(error);
35913593
} else {
35923594
inner.pendingClose.resolve?.();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Flags: --experimental-quic --no-warnings
2+
3+
// Regression test: destroying a session with an error while its `closed`
4+
// promise is not being observed must NOT surface as an unhandled rejection.
5+
6+
import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs';
7+
import { subscribe, unsubscribe } from 'node:diagnostics_channel';
8+
import { setImmediate as tick } from 'node:timers/promises';
9+
10+
if (!hasQuic) {
11+
skip('QUIC is not enabled');
12+
}
13+
14+
const { listen, connect } = await import('../common/quic.mjs');
15+
16+
// Any unhandled rejection - e.g. an unobserved `closed` rejecting - fails.
17+
process.on('unhandledRejection', mustNotCall('unexpected unhandled rejection'));
18+
19+
// We use the diagnostics channel to observe the error close without actually
20+
// observing session.closed (not listening is the whole point of the test):
21+
const serverErrored = Promise.withResolvers();
22+
function onSessionError() {
23+
unsubscribe('quic.session.error', onSessionError);
24+
serverErrored.resolve();
25+
}
26+
subscribe('quic.session.error', onSessionError);
27+
28+
// The server session callback is left deliberately empty, so no response
29+
// is sent and closed remains unobserved:
30+
const serverEndpoint = await listen(mustCall());
31+
32+
const clientSession = await connect(serverEndpoint.address);
33+
await clientSession.opened;
34+
35+
// Finish handshake before close:
36+
await tick();
37+
38+
// Cleanly close from the client with an error code, so the server
39+
// receives a peer error close:
40+
await clientSession.close({ code: 1 });
41+
42+
// Wait until the server has processed the error close, plus another tick
43+
// to ensure unobserved promise rejection doesn't fire anywhere:
44+
await serverErrored.promise;
45+
await tick();
46+
47+
await serverEndpoint.close();

0 commit comments

Comments
 (0)