Skip to content

Commit 8ab4159

Browse files
MadaraUchihatargos
authored andcommitted
http2: add support for AbortSignal to http2Session.request
- Add support - Add test - Docs once PR is up PR-URL: nodejs#36070 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent e7d3fed commit 8ab4159

File tree

3 files changed

+106
-6
lines changed

3 files changed

+106
-6
lines changed

doc/api/http2.md

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
<!-- YAML
33
added: v8.4.0
44
changes:
5+
- version: REPLACEME
6+
pr-url: https://github.com/nodejs/node/pull/36070
7+
description: It is possible to abort a request with an AbortSignal.
58
- version: v10.10.0
69
pr-url: https://github.com/nodejs/node/pull/22466
710
description: HTTP/2 is now Stable. Previously, it had been Experimental.
@@ -819,6 +822,8 @@ added: v8.4.0
819822
and `256` (inclusive).
820823
* `waitForTrailers` {boolean} When `true`, the `Http2Stream` will emit the
821824
`'wantTrailers'` event after the final `DATA` frame has been sent.
825+
* `signal` {AbortSignal} An AbortSignal that may be used to abort an ongoing
826+
request.
822827

823828
* Returns: {ClientHttp2Stream}
824829

@@ -855,6 +860,10 @@ close when the final `DATA` frame is transmitted. User code must call either
855860
`http2stream.sendTrailers()` or `http2stream.close()` to close the
856861
`Http2Stream`.
857862

863+
When `options.signal` is set with an `AbortSignal` and then `abort` on the
864+
corresponding `AbortController` is called, the request will emit an `'error'`
865+
event with an `AbortError` error.
866+
858867
The `:method` and `:path` pseudo-headers are not specified within `headers`,
859868
they respectively default to:
860869

lib/internal/http2/core.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,15 @@ const {
9494
ERR_OUT_OF_RANGE,
9595
ERR_SOCKET_CLOSED
9696
},
97-
hideStackFrames
97+
hideStackFrames,
98+
AbortError
9899
} = require('internal/errors');
99-
const { validateNumber,
100-
validateString,
101-
validateUint32,
102-
isUint32,
100+
const {
101+
isUint32,
102+
validateNumber,
103+
validateString,
104+
validateUint32,
105+
validateAbortSignal,
103106
} = require('internal/validators');
104107
const fsPromisesInternal = require('internal/fs/promises');
105108
const { utcDate } = require('internal/http');
@@ -1666,6 +1669,20 @@ class ClientHttp2Session extends Http2Session {
16661669
if (options.waitForTrailers)
16671670
stream[kState].flags |= STREAM_FLAGS_HAS_TRAILERS;
16681671

1672+
const { signal } = options;
1673+
if (signal) {
1674+
validateAbortSignal(signal, 'options.signal');
1675+
const aborter = () => stream.destroy(new AbortError());
1676+
if (signal.aborted) {
1677+
aborter();
1678+
} else {
1679+
signal.addEventListener('abort', aborter);
1680+
stream.once('close', () => {
1681+
signal.removeEventListener('abort', aborter);
1682+
});
1683+
}
1684+
}
1685+
16691686
const onConnect = requestOnConnect.bind(stream, headersList, options);
16701687
if (this.connecting) {
16711688
if (this[kPendingRequestCalls] !== null) {

test/parallel/test-http2-client-destroy.js

+75-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Flags: --expose-internals
1+
// Flags: --expose-internals --experimental-abortcontroller
22

33
'use strict';
44

@@ -8,6 +8,7 @@ if (!common.hasCrypto)
88
const assert = require('assert');
99
const h2 = require('http2');
1010
const { kSocket } = require('internal/http2/util');
11+
const { kEvents } = require('internal/event_target');
1112
const Countdown = require('../common/countdown');
1213

1314
{
@@ -165,3 +166,76 @@ const Countdown = require('../common/countdown');
165166
req.on('close', common.mustCall(() => server.close()));
166167
}));
167168
}
169+
170+
// Destroy with AbortSignal
171+
{
172+
const server = h2.createServer();
173+
const controller = new AbortController();
174+
175+
server.on('stream', common.mustNotCall());
176+
server.listen(0, common.mustCall(() => {
177+
const client = h2.connect(`http://localhost:${server.address().port}`);
178+
client.on('close', common.mustCall());
179+
180+
const { signal } = controller;
181+
assert.strictEqual(signal[kEvents].get('abort'), undefined);
182+
183+
client.on('error', common.mustCall(() => {
184+
// After underlying stream dies, signal listener detached
185+
assert.strictEqual(signal[kEvents].get('abort'), undefined);
186+
}));
187+
188+
const req = client.request({}, { signal });
189+
190+
req.on('error', common.mustCall((err) => {
191+
assert.strictEqual(err.code, 'ABORT_ERR');
192+
assert.strictEqual(err.name, 'AbortError');
193+
}));
194+
req.on('close', common.mustCall(() => server.close()));
195+
196+
assert.strictEqual(req.aborted, false);
197+
assert.strictEqual(req.destroyed, false);
198+
// Signal listener attached
199+
assert.strictEqual(signal[kEvents].get('abort').size, 1);
200+
201+
controller.abort();
202+
203+
assert.strictEqual(req.aborted, false);
204+
assert.strictEqual(req.destroyed, true);
205+
}));
206+
}
207+
// Pass an already destroyed signal to abort immediately.
208+
{
209+
const server = h2.createServer();
210+
const controller = new AbortController();
211+
212+
server.on('stream', common.mustNotCall());
213+
server.listen(0, common.mustCall(() => {
214+
const client = h2.connect(`http://localhost:${server.address().port}`);
215+
client.on('close', common.mustCall());
216+
217+
const { signal } = controller;
218+
controller.abort();
219+
220+
assert.strictEqual(signal[kEvents].get('abort'), undefined);
221+
222+
client.on('error', common.mustCall(() => {
223+
// After underlying stream dies, signal listener detached
224+
assert.strictEqual(signal[kEvents].get('abort'), undefined);
225+
}));
226+
227+
const req = client.request({}, { signal });
228+
// Signal already aborted, so no event listener attached.
229+
assert.strictEqual(signal[kEvents].get('abort'), undefined);
230+
231+
assert.strictEqual(req.aborted, false);
232+
// Destroyed on same tick as request made
233+
assert.strictEqual(req.destroyed, true);
234+
235+
req.on('error', common.mustCall((err) => {
236+
assert.strictEqual(err.code, 'ABORT_ERR');
237+
assert.strictEqual(err.name, 'AbortError');
238+
}));
239+
req.on('close', common.mustCall(() => server.close()));
240+
}));
241+
}

0 commit comments

Comments
 (0)