Skip to content

Commit 524b713

Browse files
benjamingrtargos
authored andcommitted
http: add support for abortsignal to http.request
PR-URL: #36048 Backport-PR-URL: #38386 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Denys Otrishko <[email protected]> Reviewed-By: Ricky Zhou <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 6e734a8 commit 524b713

File tree

5 files changed

+60
-5
lines changed

5 files changed

+60
-5
lines changed

doc/api/http.md

+9
Original file line numberDiff line numberDiff line change
@@ -2326,6 +2326,9 @@ This can be overridden for servers and client requests by passing the
23262326
<!-- YAML
23272327
added: v0.3.6
23282328
changes:
2329+
- version: REPLACEME
2330+
pr-url: https://github.com/nodejs/node/pull/36048
2331+
description: It is possible to abort a request with an AbortSignal.
23292332
- version:
23302333
- v13.8.0
23312334
- v12.15.0
@@ -2393,6 +2396,8 @@ changes:
23932396
or `port` is specified, those specify a TCP Socket).
23942397
* `timeout` {number}: A number specifying the socket timeout in milliseconds.
23952398
This will set the timeout before the socket is connected.
2399+
* `signal` {AbortSignal}: An AbortSignal that may be used to abort an ongoing
2400+
request.
23962401
* `callback` {Function}
23972402
* Returns: {http.ClientRequest}
23982403

@@ -2580,6 +2585,10 @@ events will be emitted in the following order:
25802585
Setting the `timeout` option or using the `setTimeout()` function will
25812586
not abort the request or do anything besides add a `'timeout'` event.
25822587

2588+
Passing an `AbortSignal` and then calling `abort` on the corresponding
2589+
`AbortController` will behave the same way as calling `.destroy()` on the
2590+
request itself.
2591+
25832592
## `http.validateHeaderName(name)`
25842593
<!-- YAML
25852594
added: v14.3.0

lib/.eslintrc.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ rules:
9595
- selector: "ThrowStatement > CallExpression[callee.name=/Error$/]"
9696
message: "Use new keyword when throwing an Error."
9797
# Config specific to lib
98-
- selector: "NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError)$/])"
98+
- selector: "NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError)$/])"
9999
message: "Use an error exported by the internal/errors module."
100100
- selector: "CallExpression[callee.object.name='Error'][callee.property.name='captureStackTrace']"
101101
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
@@ -704,6 +704,16 @@ const fatalExceptionStackEnhancers = {
704704
}
705705
};
706706

707+
// Node uses an AbortError that isn't exactly the same as the DOMException
708+
// to make usage of the error in userland and readable-stream easier.
709+
// It is a regular error with `.code` and `.name`.
710+
class AbortError extends Error {
711+
constructor() {
712+
super('The operation was aborted');
713+
this.code = 'ABORT_ERR';
714+
this.name = 'AbortError';
715+
}
716+
}
707717
module.exports = {
708718
addCodeToName, // Exported for NghttpError
709719
codes,
@@ -718,6 +728,7 @@ module.exports = {
718728
uvException,
719729
uvExceptionWithHostPort,
720730
SystemError,
731+
AbortError,
721732
// This is exported only to facilitate testing.
722733
E,
723734
kNoOverride,

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

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --experimental-abortcontroller
12
'use strict';
23
const common = require('../common');
34
const http = require('http');
@@ -52,8 +53,7 @@ const assert = require('assert');
5253
{
5354
// destroy
5455

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

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

0 commit comments

Comments
 (0)