Skip to content

Commit d2c8ebf

Browse files
committed
quic: support AbortSignal in QuicSocket connect/listen
Signed-off-by: James M Snell <[email protected]>
1 parent dccf7bb commit d2c8ebf

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

doc/api/quic.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,8 @@ added: REPLACEME
16261626
* `maxStreamDataUni` {number}
16271627
* `maxStreamsBidi` {number}
16281628
* `maxStreamsUni` {number}
1629+
* `signal` {AbortSignal} Optionally allows the `connect()` to be canceled
1630+
using an `AbortController`.
16291631
* `h3` {Object} HTTP/3 Specific Configuration Options
16301632
* `qpackMaxTableCapacity` {number}
16311633
* `qpackBlockedStreams` {number}
@@ -1830,6 +1832,8 @@ added: REPLACEME
18301832
[OpenSSL Options][].
18311833
* `sessionIdContext` {string} Opaque identifier used by servers to ensure
18321834
session state is not shared between applications. Unused by clients.
1835+
* `signal` {AbortSignal} Optionally allows the `listen()` to be canceled
1836+
using an `AbortController`.
18331837
* Returns: {Promise}
18341838

18351839
Listen for new peer-initiated sessions. Returns a `Promise` that is resolved

lib/internal/quic/core.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ class QuicEndpoint {
639639
if (state.bindPromise !== undefined)
640640
return state.bindPromise;
641641

642-
return state.bindPromise = this[kBind]().finally(() => {
642+
return state.bindPromise = this[kBind](options).finally(() => {
643643
state.bindPromise = undefined;
644644
});
645645
}
@@ -1187,6 +1187,15 @@ class QuicSocket extends EventEmitter {
11871187
...options,
11881188
};
11891189

1190+
const { signal } = options;
1191+
if (signal != null && !('aborted' in signal))
1192+
throw new ERR_INVALID_ARG_TYPE('options.signal', 'AbortSignal', signal);
1193+
1194+
// If an AbortSignal was passed in, check to make sure it is not already
1195+
// aborted before we continue on to do any work.
1196+
if (signal && signal.aborted)
1197+
throw new lazyDOMException('The operation was aborted', 'AbortError');
1198+
11901199
// The ALPN protocol identifier is strictly required.
11911200
const {
11921201
alpn,
@@ -1211,7 +1220,10 @@ class QuicSocket extends EventEmitter {
12111220
state.ocspHandler = ocspHandler;
12121221
state.clientHelloHandler = clientHelloHandler;
12131222

1214-
await this[kMaybeBind]();
1223+
await this[kMaybeBind]({ signal });
1224+
1225+
if (signal && signal.aborted)
1226+
throw new lazyDOMException('The operation was aborted', 'AbortError');
12151227

12161228
// It's possible that the QuicSocket was destroyed or closed while
12171229
// the bind was pending. Check for that and handle accordingly.
@@ -1226,6 +1238,9 @@ class QuicSocket extends EventEmitter {
12261238
type
12271239
} = await resolvePreferredAddress(lookup, transportParams.preferredAddress);
12281240

1241+
if (signal && signal.aborted)
1242+
throw new lazyDOMException('The operation was aborted', 'AbortError');
1243+
12291244
// It's possible that the QuicSocket was destroyed or closed while
12301245
// the preferred address resolution was pending. Check for that and handle
12311246
// accordingly.
@@ -1264,6 +1279,14 @@ class QuicSocket extends EventEmitter {
12641279
// while the nextTick is pending. If that happens, do nothing.
12651280
if (this.destroyed || this.closing)
12661281
return;
1282+
1283+
// The abort signal was triggered while this was pending,
1284+
// destroy the QuicSocket with an error.
1285+
if (signal && signal.aborted) {
1286+
this.destroy(
1287+
new lazyDOMException('The operation was aborted', 'AbortError'));
1288+
return;
1289+
}
12671290
try {
12681291
this.emit('listening');
12691292
} catch (error) {
@@ -1284,13 +1307,25 @@ class QuicSocket extends EventEmitter {
12841307
...options
12851308
};
12861309

1310+
const { signal } = options;
1311+
if (signal != null && !('aborted' in signal))
1312+
throw new ERR_INVALID_ARG_TYPE('options.signal', 'AbortSignal', signal);
1313+
1314+
// If an AbortSignal was passed in, check to make sure it is not already
1315+
// aborted before we continue on to do any work.
1316+
if (signal && signal.aborted)
1317+
throw new lazyDOMException('The operation was aborted', 'AbortError');
1318+
12871319
const {
12881320
type,
12891321
address,
12901322
lookup = state.lookup
12911323
} = validateQuicSocketConnectOptions(options);
12921324

1293-
await this[kMaybeBind]();
1325+
await this[kMaybeBind]({ signal });
1326+
1327+
if (signal && signal.aborted)
1328+
throw new lazyDOMException('The operation was aborted', 'AbortError');
12941329

12951330
if (this.destroyed)
12961331
throw new ERR_INVALID_STATE('QuicSocket was destroyed');
@@ -1302,6 +1337,9 @@ class QuicSocket extends EventEmitter {
13021337
} = await lookup(addressOrLocalhost(address, type),
13031338
type === AF_INET6 ? 6 : 4);
13041339

1340+
if (signal && signal.aborted)
1341+
throw new lazyDOMException('The operation was aborted', 'AbortError');
1342+
13051343
if (this.destroyed)
13061344
throw new ERR_INVALID_STATE('QuicSocket was destroyed');
13071345
if (this.closing)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Flags: --no-warnings
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!common.hasQuic)
6+
common.skip('missing quic');
7+
8+
const assert = require('assert');
9+
10+
const { createQuicSocket } = require('net');
11+
12+
{
13+
const socket = createQuicSocket();
14+
const ac = new AbortController();
15+
16+
// Abort before call.
17+
ac.abort();
18+
19+
assert.rejects(socket.connect({ signal: ac.signal }), {
20+
name: 'AbortError'
21+
});
22+
assert.rejects(socket.listen({ signal: ac.signal }), {
23+
name: 'AbortError'
24+
});
25+
}
26+
27+
{
28+
const socket = createQuicSocket();
29+
const ac = new AbortController();
30+
31+
assert.rejects(socket.connect({ signal: ac.signal }), {
32+
name: 'AbortError'
33+
});
34+
assert.rejects(socket.listen({ signal: ac.signal }), {
35+
name: 'AbortError'
36+
});
37+
38+
// Abort after call, not awaiting previously created Promises.
39+
ac.abort();
40+
}
41+
42+
{
43+
const socket = createQuicSocket();
44+
const ac = new AbortController();
45+
46+
async function lookup() {
47+
ac.abort();
48+
return { address: '1.1.1.1' };
49+
}
50+
51+
assert.rejects(
52+
socket.connect({ address: 'foo', lookup, signal: ac.signal }), {
53+
name: 'AbortError'
54+
});
55+
56+
assert.rejects(
57+
socket.listen({
58+
preferredAddress: { address: 'foo' },
59+
lookup,
60+
signal: ac.signal }), {
61+
name: 'AbortError'
62+
});
63+
}

0 commit comments

Comments
 (0)