Skip to content

Commit 8390f8a

Browse files
benjamingrcodebytere
authored andcommittedNov 22, 2020
http: add support for abortsignal to http.request
PR-URL: #36048 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Denys Otrishko <shishugi@gmail.com> Reviewed-By: Ricky Zhou <0x19951125@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 82f1cde commit 8390f8a

File tree

5 files changed

+59
-5
lines changed

5 files changed

+59
-5
lines changed
 

‎doc/api/http.md

+9
Original file line numberDiff line numberDiff line change
@@ -2336,6 +2336,9 @@ This can be overridden for servers and client requests by passing the
23362336
<!-- YAML
23372337
added: v0.3.6
23382338
changes:
2339+
- version: REPLACEME
2340+
pr-url: https://github.com/nodejs/node/pull/36048
2341+
description: It is possible to abort a request with an AbortSignal.
23392342
- version:
23402343
- v13.8.0
23412344
- v12.15.0
@@ -2403,6 +2406,8 @@ changes:
24032406
or `port` is specified, those specify a TCP Socket).
24042407
* `timeout` {number}: A number specifying the socket timeout in milliseconds.
24052408
This will set the timeout before the socket is connected.
2409+
* `signal` {AbortSignal}: An AbortSignal that may be used to abort an ongoing
2410+
request.
24062411
* `callback` {Function}
24072412
* Returns: {http.ClientRequest}
24082413

@@ -2596,6 +2601,10 @@ events will be emitted in the following order:
25962601
Setting the `timeout` option or using the `setTimeout()` function will
25972602
not abort the request or do anything besides add a `'timeout'` event.
25982603

2604+
Passing an `AbortSignal` and then calling `abort` on the corresponding
2605+
`AbortController` will behave the same way as calling `.destroy()` on the
2606+
request itself.
2607+
25992608
## `http.validateHeaderName(name)`
26002609
<!-- YAML
26012610
added: v14.3.0

‎lib/.eslintrc.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ rules:
1919
- selector: "ThrowStatement > CallExpression[callee.name=/Error$/]"
2020
message: "Use new keyword when throwing an Error."
2121
# Config specific to lib
22-
- selector: "NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError)$/])"
22+
- selector: "NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError)$/])"
2323
message: "Use an error exported by the internal/errors module."
2424
- selector: "CallExpression[callee.object.name='Error'][callee.property.name='captureStackTrace']"
2525
message: "Please use `require('internal/errors').hideStackFrames()` instead."

‎lib/_http_client.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@ const { Buffer } = require('buffer');
5151
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
5252
const { URL, urlToOptions, searchParamsSymbol } = require('internal/url');
5353
const { kOutHeaders, kNeedDrain } = require('internal/http');
54-
const { connResetException, codes } = require('internal/errors');
54+
const { AbortError, connResetException, codes } = require('internal/errors');
5555
const {
5656
ERR_HTTP_HEADERS_SENT,
5757
ERR_INVALID_ARG_TYPE,
5858
ERR_INVALID_HTTP_TOKEN,
5959
ERR_INVALID_PROTOCOL,
6060
ERR_UNESCAPED_CHARACTERS
6161
} = codes;
62-
const { validateInteger } = require('internal/validators');
62+
const {
63+
validateInteger,
64+
validateAbortSignal,
65+
} = require('internal/validators');
6366
const { getTimerDuration } = require('internal/timers');
6467
const {
6568
DTRACE_HTTP_CLIENT_REQUEST,
@@ -169,6 +172,15 @@ function ClientRequest(input, options, cb) {
169172
if (options.timeout !== undefined)
170173
this.timeout = getTimerDuration(options.timeout, 'timeout');
171174

175+
const signal = options.signal;
176+
if (signal) {
177+
validateAbortSignal(signal, 'options.signal');
178+
const listener = (e) => this.destroy(new AbortError());
179+
signal.addEventListener('abort', listener);
180+
this.once('close', () => {
181+
signal.removeEventListener('abort', listener);
182+
});
183+
}
172184
let method = options.method;
173185
const methodIsString = (typeof method === 'string');
174186
if (method !== null && method !== undefined && !methodIsString) {

‎lib/internal/errors.js

+11
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,16 @@ const fatalExceptionStackEnhancers = {
727727
}
728728
};
729729

730+
// Node uses an AbortError that isn't exactly the same as the DOMException
731+
// to make usage of the error in userland and readable-stream easier.
732+
// It is a regular error with `.code` and `.name`.
733+
class AbortError extends Error {
734+
constructor() {
735+
super('The operation was aborted');
736+
this.code = 'ABORT_ERR';
737+
this.name = 'AbortError';
738+
}
739+
}
730740
module.exports = {
731741
addCodeToName, // Exported for NghttpError
732742
codes,
@@ -741,6 +751,7 @@ module.exports = {
741751
uvException,
742752
uvExceptionWithHostPort,
743753
SystemError,
754+
AbortError,
744755
// This is exported only to facilitate testing.
745756
E,
746757
kNoOverride,

‎test/parallel/test-http-client-abort-destroy.js

+24-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ const assert = require('assert');
5252
{
5353
// destroy
5454

55-
const server = http.createServer(common.mustNotCall((req, res) => {
56-
}));
55+
const server = http.createServer(common.mustNotCall());
5756

5857
server.listen(0, common.mustCall(() => {
5958
const options = { port: server.address().port };
@@ -69,3 +68,26 @@ const assert = require('assert');
6968
assert.strictEqual(req.destroyed, true);
7069
}));
7170
}
71+
72+
73+
{
74+
// Destroy with AbortSignal
75+
76+
const server = http.createServer(common.mustNotCall());
77+
const controller = new AbortController();
78+
79+
server.listen(0, common.mustCall(() => {
80+
const options = { port: server.address().port, signal: controller.signal };
81+
const req = http.get(options, common.mustNotCall());
82+
req.on('error', common.mustCall((err) => {
83+
assert.strictEqual(err.code, 'ABORT_ERR');
84+
assert.strictEqual(err.name, 'AbortError');
85+
server.close();
86+
}));
87+
assert.strictEqual(req.aborted, false);
88+
assert.strictEqual(req.destroyed, false);
89+
controller.abort();
90+
assert.strictEqual(req.aborted, false);
91+
assert.strictEqual(req.destroyed, true);
92+
}));
93+
}

0 commit comments

Comments
 (0)
Please sign in to comment.