Files
node/test/parallel/test-http-response-socket-error.js
T
XadillaX 5ebf690dcf http: preserve socket errors as response error causes
Keep the original socket error as the cause of ECONNRESET errors emitted
when an HTTP response closes before completion. Preserve the existing
aborted message, error code, and event order. Leave cause absent when
there is no underlying error.

Allow ConnResetException to accept Error options, document the behavior,
and cover TLS record errors, TCP resets, explicit destruction, and
premature closure without a socket error.

This follows investigation of #66001 and addresses lost error context.
The original TLS decryption failure remains unresolved.

Refs: https://github.com/nodejs/node/issues/66001
Signed-off-by: XadillaX <i@2333.moe>
PR-URL: https://github.com/nodejs/node/pull/66061
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
2026-09-25 21:31:04 +02:00

83 lines
2.6 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const { Writable, pipeline } = require('stream');
for (const method of ['emit', 'emitAndDestroy', 'socketDestroy', 'requestDestroy', 'reset', 'close']) {
let error = method === 'close' || method === 'reset' ? null : new Error('Socket failure');
let serverSocket;
const server = http.createServer(common.mustCall((req, res) => {
serverSocket = req.socket;
res.writeHead(200, { 'Content-Length': 100 });
res.write('partial body');
}));
server.listen(0, common.mustCall(() => {
const req = http.get({
port: server.address().port,
agent: false,
}, common.mustCall((res) => {
res.on('aborted', common.mustCall());
res.on('close', common.mustCall());
pipeline(res, new Writable({
write(chunk, encoding, callback) {
callback();
},
}), common.mustCall((err) => {
assert.strictEqual(res.complete, false);
assert.strictEqual(res.aborted, true);
assert.strictEqual(err.code, 'ECONNRESET');
assert.strictEqual(err.message, 'aborted');
assert.strictEqual(res.errored, err);
if (error) {
assert.notStrictEqual(err, error);
assert.strictEqual(err.cause, error);
assert.deepStrictEqual(Object.getOwnPropertyDescriptor(err, 'cause'), {
value: error,
writable: true,
enumerable: false,
configurable: true,
});
} else {
assert.strictEqual(Object.hasOwn(err, 'cause'), false);
}
server.close(common.mustCall());
}));
switch (method) {
case 'emit':
case 'emitAndDestroy':
// TLSSocket can emit an error without setting socket.errored.
req.socket.emit('error', error);
break;
case 'socketDestroy':
req.socket.destroy(error);
break;
case 'requestDestroy':
req.destroy(error);
break;
case 'reset':
serverSocket.resetAndDestroy();
break;
case 'close':
req.socket.destroy();
break;
}
}));
req.on('error', method !== 'close' ? common.mustCall((err) => {
if (method === 'reset') {
assert.strictEqual(err.code, 'ECONNRESET');
assert.strictEqual(err.syscall, 'read');
error = err;
} else {
assert.strictEqual(err, error);
}
if (method === 'emitAndDestroy')
req.destroy();
}) : common.mustNotCall());
req.on('close', common.mustCall());
}));
}