Skip to content

Commit

Permalink
quic: support AbortSignal in QuicSocket connect/listen
Browse files Browse the repository at this point in the history
Signed-off-by: James M Snell <[email protected]>
  • Loading branch information
jasnell committed Aug 24, 2020
1 parent dccf7bb commit d2c8ebf
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 3 deletions.
4 changes: 4 additions & 0 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,8 @@ added: REPLACEME
* `maxStreamDataUni` {number}
* `maxStreamsBidi` {number}
* `maxStreamsUni` {number}
* `signal` {AbortSignal} Optionally allows the `connect()` to be canceled
using an `AbortController`.
* `h3` {Object} HTTP/3 Specific Configuration Options
* `qpackMaxTableCapacity` {number}
* `qpackBlockedStreams` {number}
Expand Down Expand Up @@ -1830,6 +1832,8 @@ added: REPLACEME
[OpenSSL Options][].
* `sessionIdContext` {string} Opaque identifier used by servers to ensure
session state is not shared between applications. Unused by clients.
* `signal` {AbortSignal} Optionally allows the `listen()` to be canceled
using an `AbortController`.
* Returns: {Promise}

Listen for new peer-initiated sessions. Returns a `Promise` that is resolved
Expand Down
44 changes: 41 additions & 3 deletions lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ class QuicEndpoint {
if (state.bindPromise !== undefined)
return state.bindPromise;

return state.bindPromise = this[kBind]().finally(() => {
return state.bindPromise = this[kBind](options).finally(() => {
state.bindPromise = undefined;
});
}
Expand Down Expand Up @@ -1187,6 +1187,15 @@ class QuicSocket extends EventEmitter {
...options,
};

const { signal } = options;
if (signal != null && !('aborted' in signal))
throw new ERR_INVALID_ARG_TYPE('options.signal', 'AbortSignal', signal);

// If an AbortSignal was passed in, check to make sure it is not already
// aborted before we continue on to do any work.
if (signal && signal.aborted)
throw new lazyDOMException('The operation was aborted', 'AbortError');

// The ALPN protocol identifier is strictly required.
const {
alpn,
Expand All @@ -1211,7 +1220,10 @@ class QuicSocket extends EventEmitter {
state.ocspHandler = ocspHandler;
state.clientHelloHandler = clientHelloHandler;

await this[kMaybeBind]();
await this[kMaybeBind]({ signal });

if (signal && signal.aborted)
throw new lazyDOMException('The operation was aborted', 'AbortError');

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

if (signal && signal.aborted)
throw new lazyDOMException('The operation was aborted', 'AbortError');

// It's possible that the QuicSocket was destroyed or closed while
// the preferred address resolution was pending. Check for that and handle
// accordingly.
Expand Down Expand Up @@ -1264,6 +1279,14 @@ class QuicSocket extends EventEmitter {
// while the nextTick is pending. If that happens, do nothing.
if (this.destroyed || this.closing)
return;

// The abort signal was triggered while this was pending,
// destroy the QuicSocket with an error.
if (signal && signal.aborted) {
this.destroy(
new lazyDOMException('The operation was aborted', 'AbortError'));
return;
}
try {
this.emit('listening');
} catch (error) {
Expand All @@ -1284,13 +1307,25 @@ class QuicSocket extends EventEmitter {
...options
};

const { signal } = options;
if (signal != null && !('aborted' in signal))
throw new ERR_INVALID_ARG_TYPE('options.signal', 'AbortSignal', signal);

// If an AbortSignal was passed in, check to make sure it is not already
// aborted before we continue on to do any work.
if (signal && signal.aborted)
throw new lazyDOMException('The operation was aborted', 'AbortError');

const {
type,
address,
lookup = state.lookup
} = validateQuicSocketConnectOptions(options);

await this[kMaybeBind]();
await this[kMaybeBind]({ signal });

if (signal && signal.aborted)
throw new lazyDOMException('The operation was aborted', 'AbortError');

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

if (signal && signal.aborted)
throw new lazyDOMException('The operation was aborted', 'AbortError');

if (this.destroyed)
throw new ERR_INVALID_STATE('QuicSocket was destroyed');
if (this.closing)
Expand Down
63 changes: 63 additions & 0 deletions test/parallel/test-quic-quicsocket-abortsignal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Flags: --no-warnings
'use strict';

const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');

const assert = require('assert');

const { createQuicSocket } = require('net');

{
const socket = createQuicSocket();
const ac = new AbortController();

// Abort before call.
ac.abort();

assert.rejects(socket.connect({ signal: ac.signal }), {
name: 'AbortError'
});
assert.rejects(socket.listen({ signal: ac.signal }), {
name: 'AbortError'
});
}

{
const socket = createQuicSocket();
const ac = new AbortController();

assert.rejects(socket.connect({ signal: ac.signal }), {
name: 'AbortError'
});
assert.rejects(socket.listen({ signal: ac.signal }), {
name: 'AbortError'
});

// Abort after call, not awaiting previously created Promises.
ac.abort();
}

{
const socket = createQuicSocket();
const ac = new AbortController();

async function lookup() {
ac.abort();
return { address: '1.1.1.1' };
}

assert.rejects(
socket.connect({ address: 'foo', lookup, signal: ac.signal }), {
name: 'AbortError'
});

assert.rejects(
socket.listen({
preferredAddress: { address: 'foo' },
lookup,
signal: ac.signal }), {
name: 'AbortError'
});
}

0 comments on commit d2c8ebf

Please sign in to comment.