Skip to content

Commit fa6eb73

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

2 files changed

Lines changed: 48 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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
import { listen, connect } from '../common/quic.mjs';
10+
11+
if (!hasQuic) {
12+
skip('QUIC is not enabled');
13+
}
14+
15+
// Any unhandled rejection - e.g. an unobserved `closed` rejecting - fails.
16+
process.on('unhandledRejection', mustNotCall('unexpected unhandled rejection'));
17+
18+
// We use the diagnostics channel to observe the error close without actually
19+
// observing session.closed (not listening is the whole point of the test):
20+
const serverErrored = Promise.withResolvers();
21+
function onSessionError() {
22+
unsubscribe('quic.session.error', onSessionError);
23+
serverErrored.resolve();
24+
}
25+
subscribe('quic.session.error', onSessionError);
26+
27+
// The server session is deliberately left unobserved:
28+
const serverEndpoint = await listen(mustCall(() => {}));
29+
30+
const clientSession = await connect(serverEndpoint.address);
31+
await clientSession.opened;
32+
33+
// Finish handshake before close:
34+
await tick();
35+
36+
// Cleanly close from the client with an error code, so the server
37+
// receives a peer error close:
38+
await clientSession.close({ code: 1 });
39+
40+
// Wait until the server has processed the error close, plus another tick
41+
// to ensure unobserved promise rejection doesn't fire anywhere:
42+
await serverErrored.promise;
43+
await tick();
44+
45+
await serverEndpoint.close();

0 commit comments

Comments
 (0)