Skip to content

Commit 8a6fab0

Browse files
committed
http: emit 'error' on aborted server request
Server requests aka. IncomingMessage emits 'aborted' instead of 'error' which causes confusion when the object is used as a regular stream, i.e. if functions working on streams are passed a server request object they might not work properly unless they take this into account. Refs: nodejs/web-server-frameworks#41 PR-URL: #33172 Fixes: #28172 Refs: #28677 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent cbf2fa6 commit 8a6fab0

8 files changed

+121
-35
lines changed

doc/api/http.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,8 @@ Until the data is consumed, the `'end'` event will not fire. Also, until
333333
the data is read it will consume memory that can eventually lead to a
334334
'process out of memory' error.
335335

336-
Unlike the `request` object, if the response closes prematurely, the
337-
`response` object does not emit an `'error'` event but instead emits the
338-
`'aborted'` event.
336+
For backward compatibility, `res` will only emit `'error'` if there is an
337+
`'error'` listener registered.
339338

340339
Node.js does not check whether Content-Length and the length of the
341340
body which has been transmitted are equal or not.
@@ -2417,6 +2416,8 @@ the following events will be emitted in the following order:
24172416
* `'data'` any number of times, on the `res` object
24182417
* (connection closed here)
24192418
* `'aborted'` on the `res` object
2419+
* `'error'` on the `res` object with an error with message
2420+
`'Error: aborted'` and code `'ECONNRESET'`.
24202421
* `'close'`
24212422
* `'close'` on the `res` object
24222423

@@ -2445,6 +2446,8 @@ events will be emitted in the following order:
24452446
* `'data'` any number of times, on the `res` object
24462447
* (`req.destroy()` called here)
24472448
* `'aborted'` on the `res` object
2449+
* `'error'` on the `res` object with an error with message
2450+
`'Error: aborted'` and code `'ECONNRESET'`.
24482451
* `'close'`
24492452
* `'close'` on the `res` object
24502453

@@ -2474,6 +2477,8 @@ events will be emitted in the following order:
24742477
* (`req.abort()` called here)
24752478
* `'abort'`
24762479
* `'aborted'` on the `res` object
2480+
* `'error'` on the `res` object with an error with message
2481+
`'Error: aborted'` and code `'ECONNRESET'`.
24772482
* `'close'`
24782483
* `'close'` on the `res` object
24792484

lib/_http_client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,13 @@ function socketCloseListener() {
417417
const res = req.res;
418418
if (res) {
419419
// Socket closed before we emitted 'end' below.
420+
// TOOD(ronag): res.destroy(err)
420421
if (!res.complete) {
421422
res.aborted = true;
422423
res.emit('aborted');
424+
if (res.listenerCount('error') > 0) {
425+
res.emit('error', connResetException('aborted'));
426+
}
423427
}
424428
req.emit('close');
425429
if (!res.aborted && res.readable) {

lib/_http_server.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,16 @@ const {
5656
getOrSetAsyncId
5757
} = require('internal/async_hooks');
5858
const { IncomingMessage } = require('_http_incoming');
59+
const {
60+
connResetException,
61+
codes
62+
} = require('internal/errors');
5963
const {
6064
ERR_HTTP_HEADERS_SENT,
6165
ERR_HTTP_INVALID_STATUS_CODE,
6266
ERR_INVALID_ARG_TYPE,
6367
ERR_INVALID_CHAR
64-
} = require('internal/errors').codes;
68+
} = codes;
6569
const { validateInteger } = require('internal/validators');
6670
const Buffer = require('buffer').Buffer;
6771
const {
@@ -536,9 +540,13 @@ function socketOnClose(socket, state) {
536540
function abortIncoming(incoming) {
537541
while (incoming.length) {
538542
const req = incoming.shift();
543+
// TODO(ronag): req.destroy(err)
539544
req.aborted = true;
540545
req.destroyed = true;
541546
req.emit('aborted');
547+
if (req.listenerCount('error') > 0) {
548+
req.emit('error', connResetException('aborted'));
549+
}
542550
req.emit('close');
543551
}
544552
// Abort socket._httpMessage ?

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ server.listen(0, common.mustCall(() => {
4141
res.resume();
4242
res.on('end', common.mustNotCall());
4343
res.on('aborted', common.mustCall());
44+
res.on('error', common.expectsError({
45+
code: 'ECONNRESET'
46+
}));
4447
res.on('close', common.mustCall());
4548
res.socket.on('close', common.mustCall());
4649
}));

test/parallel/test-http-aborted.js

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,58 @@ const common = require('../common');
44
const http = require('http');
55
const assert = require('assert');
66

7-
const server = http.createServer(common.mustCall(function(req, res) {
8-
req.on('aborted', common.mustCall(function() {
9-
assert.strictEqual(this.aborted, true);
10-
server.close();
7+
{
8+
const server = http.createServer(common.mustCall(function(req, res) {
9+
req.on('aborted', common.mustCall(function() {
10+
assert.strictEqual(this.aborted, true);
11+
}));
12+
req.on('error', common.mustCall(function(err) {
13+
assert.strictEqual(err.code, 'ECONNRESET');
14+
assert.strictEqual(err.message, 'aborted');
15+
server.close();
16+
}));
17+
assert.strictEqual(req.aborted, false);
18+
res.write('hello');
19+
}));
20+
21+
server.listen(0, common.mustCall(() => {
22+
const req = http.get({
23+
port: server.address().port,
24+
headers: { connection: 'keep-alive' }
25+
}, common.mustCall((res) => {
26+
res.on('aborted', common.mustCall(() => {
27+
assert.strictEqual(res.aborted, true);
28+
}));
29+
res.on('error', common.expectsError({
30+
code: 'ECONNRESET',
31+
message: 'aborted'
32+
}));
33+
req.abort();
34+
}));
35+
}));
36+
}
37+
38+
{
39+
// Don't crash if no 'error' handler on server request.
40+
41+
const server = http.createServer(common.mustCall(function(req, res) {
42+
req.on('aborted', common.mustCall(function() {
43+
assert.strictEqual(this.aborted, true);
44+
server.close();
45+
}));
46+
assert.strictEqual(req.aborted, false);
47+
res.write('hello');
1148
}));
12-
assert.strictEqual(req.aborted, false);
13-
res.write('hello');
14-
}));
1549

16-
server.listen(0, common.mustCall(() => {
17-
const req = http.get({
18-
port: server.address().port,
19-
headers: { connection: 'keep-alive' }
20-
}, common.mustCall((res) => {
21-
res.on('aborted', common.mustCall(() => {
22-
assert.strictEqual(res.aborted, true);
50+
server.listen(0, common.mustCall(() => {
51+
const req = http.get({
52+
port: server.address().port,
53+
headers: { connection: 'keep-alive' }
54+
}, common.mustCall((res) => {
55+
res.on('aborted', common.mustCall(() => {
56+
assert.strictEqual(res.aborted, true);
57+
}));
58+
req.abort();
2359
}));
24-
req.abort();
2560
}));
26-
}));
61+
}

test/parallel/test-http-client-aborted-event.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,44 @@
22
const common = require('../common');
33
const http = require('http');
44

5-
let serverRes;
6-
const server = http.Server(function(req, res) {
7-
res.write('Part of my res.');
8-
serverRes = res;
9-
});
5+
{
6+
let serverRes;
7+
const server = http.Server(function(req, res) {
8+
res.write('Part of my res.');
9+
serverRes = res;
10+
});
1011

11-
server.listen(0, common.mustCall(function() {
12-
http.get({
13-
port: this.address().port,
14-
headers: { connection: 'keep-alive' }
15-
}, common.mustCall(function(res) {
16-
server.close();
17-
serverRes.destroy();
18-
res.on('aborted', common.mustCall());
12+
server.listen(0, common.mustCall(function() {
13+
http.get({
14+
port: this.address().port,
15+
headers: { connection: 'keep-alive' }
16+
}, common.mustCall(function(res) {
17+
server.close();
18+
serverRes.destroy();
19+
res.on('aborted', common.mustCall());
20+
res.on('error', common.expectsError({
21+
code: 'ECONNRESET'
22+
}));
23+
}));
1924
}));
20-
}));
25+
}
26+
27+
{
28+
// Don't crash of no 'error' handler.
29+
let serverRes;
30+
const server = http.Server(function(req, res) {
31+
res.write('Part of my res.');
32+
serverRes = res;
33+
});
34+
35+
server.listen(0, common.mustCall(function() {
36+
http.get({
37+
port: this.address().port,
38+
headers: { connection: 'keep-alive' }
39+
}, common.mustCall(function(res) {
40+
server.close();
41+
serverRes.destroy();
42+
res.on('aborted', common.mustCall());
43+
}));
44+
}));
45+
}

test/parallel/test-http-client-spurious-aborted.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,18 @@ function download() {
6060
res.on('end', common.mustCall(() => {
6161
reqCountdown.dec();
6262
}));
63+
res.on('error', common.mustNotCall());
6364
} else {
6465
res.on('aborted', common.mustCall(() => {
6566
aborted = true;
6667
reqCountdown.dec();
6768
writable.end();
6869
}));
70+
res.on('error', common.expectsError({
71+
code: 'ECONNRESET'
72+
}));
6973
}
7074

71-
res.on('error', common.mustNotCall());
7275
writable.on('finish', () => {
7376
assert.strictEqual(aborted, abortRequest);
7477
finishCountdown.dec();

test/parallel/test-http-outgoing-message-capture-rejection.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ events.captureRejections = true;
3333

3434
req.on('response', common.mustCall((res) => {
3535
res.on('aborted', common.mustCall());
36+
res.on('error', common.expectsError({
37+
code: 'ECONNRESET'
38+
}));
3639
res.resume();
3740
server.close();
3841
}));

0 commit comments

Comments
 (0)