Files
node/test/parallel/test-http-outgoing-writableFinished.js
Tim Perry 2ddade631a http: fix writableFinished and 'finish' after write errors
Only emit 'finish' and set writableFinished once all data has actually
been flushed successfully. end() callbacks now report the outcome like
stream.Writable: called with null on finish, or with the error that
prevented the flush.

Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64847
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2026-08-04 09:07:43 +00:00

132 lines
3.9 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const { Duplex } = require('stream');
// writableFinished becomes true once all data has been flushed, immediately
// before 'finish' is emitted.
{
const server = http.createServer(common.mustCall(function(req, res) {
assert.strictEqual(res.writableFinished, false);
res
.on('finish', common.mustCall(() => {
assert.strictEqual(res.writableFinished, true);
server.close();
}))
.end();
}));
server.listen(0);
server.on('listening', common.mustCall(function() {
const clientRequest = http.request({
port: server.address().port,
method: 'GET',
path: '/'
});
assert.strictEqual(clientRequest.writableFinished, false);
clientRequest
.on('finish', common.mustCall(() => {
assert.strictEqual(clientRequest.writableFinished, true);
}))
.end();
assert.strictEqual(clientRequest.writableFinished, false);
}));
}
// A request whose writes fail never becomes writableFinished and never emits
// 'finish'; the end() callback receives the write error instead.
{
const writeError = new Error('forced write failure');
const socket = new Duplex({
read() {},
write(chunk, encoding, callback) {
callback(writeError);
},
});
const failedRequest = http.request({
createConnection: common.mustCall(() => socket),
method: 'POST',
});
failedRequest.on('finish', common.mustNotCall());
failedRequest.on('error', common.mustCall((err) => {
assert.strictEqual(err, writeError);
}));
failedRequest.on('close', common.mustCall(() => {
assert.strictEqual(failedRequest.writableFinished, false);
}));
failedRequest.write('body', common.mustCall((err) => {
assert.strictEqual(err, writeError);
}));
failedRequest.end(common.mustCall((err) => {
assert.ok(err instanceof Error);
assert.strictEqual(failedRequest.writableFinished, false);
// Ending again after the flush has failed still reports the failure.
failedRequest.end(common.mustCall((endAgainErr) => {
assert.strictEqual(endAgainErr, err);
}));
}));
}
// The same for a server response whose flush fails (e.g. the connection is
// reset mid-flush). Unlike the client case, the error here only ever
// surfaces through the socket write callbacks.
{
const writeError = new Error('forced write failure');
const socket = new Duplex({
read() {},
write(chunk, encoding, callback) {
callback(writeError);
},
});
const server = http.createServer(common.mustCall((req, res) => {
res.on('finish', common.mustNotCall());
res.on('close', common.mustCall(() => {
assert.strictEqual(res.writableFinished, false);
}));
res.end('hello', common.mustCall((err) => {
assert.strictEqual(err, writeError);
assert.strictEqual(res.writableFinished, false);
}));
}));
server.emit('connection', socket);
socket.push('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
}
// The same when end() happens after the failed write, with no data left to
// flush: the write failure must still be detected even though end() itself
// has nothing to send.
{
const writeError = new Error('forced write failure');
const socket = new Duplex({
read() {},
write(chunk, encoding, callback) {
callback(writeError);
},
});
const server = http.createServer(common.mustCall((req, res) => {
res.on('finish', common.mustNotCall());
res.setHeader('Content-Length', '5');
res.write('hello', common.mustCall((err) => {
assert.strictEqual(err, writeError);
}));
setImmediate(common.mustCall(() => {
res.end(common.mustCall((err) => {
assert.strictEqual(err, writeError);
assert.strictEqual(res.writableFinished, false);
}));
}));
}));
server.emit('connection', socket);
socket.push('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
}