From dbf032b33667da4076eabcb4c8b04cf5be79e9c9 Mon Sep 17 00:00:00 2001 From: Martin Slota Date: Tue, 13 Aug 2024 21:53:53 +0200 Subject: [PATCH 1/3] Add tests demonstrating scenarios under which the original res.end does not get called --- test/compression.js | 121 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/compression.js b/test/compression.js index 6975ea0b..04e0bca7 100644 --- a/test/compression.js +++ b/test/compression.js @@ -4,6 +4,7 @@ var Buffer = require('safe-buffer').Buffer var bytes = require('bytes') var crypto = require('crypto') var http = require('http') +var net = require('net') var request = require('supertest') var zlib = require('zlib') @@ -657,6 +658,113 @@ describe('compression()', function () { .end() }) }) + + describe('when the client closes the connection before consuming the response', function () { + it('should call the original res.end() if connection is cut early on', function (done) { + var server = http.createServer(function (req, res) { + var originalResEnd = res.end + var originalResEndCalledTimes = 0 + res.end = function () { + originalResEndCalledTimes++ + return originalResEnd.apply(this, arguments) + } + + compression({ threshold: 0 })(req, res, function () { + socket.end() + + res.setHeader('Content-Type', 'text/plain') + res.write('hello, ') + setTimeout(function () { + res.end('world!') + + setTimeout(function () { + server.close(function () { + if (originalResEndCalledTimes === 1) { + done() + } else { + done(new Error('The original res.end() was called ' + originalResEndCalledTimes + ' times')) + } + }) + }, 5) + }, 5) + }) + }) + + server.listen() + + var port = server.address().port + var socket = openSocketWithRequest(port) + }) + + it('should call the original res.end() if connection is cut after an initial write', function (done) { + var server = http.createServer(function (req, res) { + var originalResEnd = res.end + var originalResEndCalledTimes = 0 + res.end = function () { + originalResEndCalledTimes++ + return originalResEnd.apply(this, arguments) + } + + compression({ threshold: 0 })(req, res, function () { + res.setHeader('Content-Type', 'text/plain') + res.write('hello, ') + socket.end() + + setTimeout(function () { + res.end('world!') + + setTimeout(function () { + server.close(function () { + if (originalResEndCalledTimes === 1) { + done() + } else { + done(new Error('The original res.end() was called ' + originalResEndCalledTimes + ' times')) + } + }) + }, 5) + }, 5) + }) + }) + + server.listen() + + var port = server.address().port + var socket = openSocketWithRequest(port) + }) + + it('should call the original res.end() if connection is cut just after response body was generated', function (done) { + var server = http.createServer(function (req, res) { + var originalResEnd = res.end + var originalResEndCalledTimes = 0 + res.end = function () { + originalResEndCalledTimes++ + return originalResEnd.apply(this, arguments) + } + + compression({ threshold: 0 })(req, res, function () { + res.setHeader('Content-Type', 'text/plain') + res.write('hello, ') + res.end('world!') + socket.end() + + setTimeout(function () { + server.close(function () { + if (originalResEndCalledTimes === 1) { + done() + } else { + done(new Error('The original res.end() was called ' + originalResEndCalledTimes + ' times')) + } + }) + }, 5) + }) + }) + + server.listen() + + var port = server.address().port + var socket = openSocketWithRequest(port) + }) + }) }) function createServer (opts, fn) { @@ -716,3 +824,16 @@ function unchunk (encoding, onchunk, onend) { stream.on('end', onend) } } + +function openSocketWithRequest (port) { + var socket = net.connect(port, function onConnect () { + socket.write('GET / HTTP/1.1\r\n') + socket.write('Accept-Encoding: gzip\r\n') + socket.write('Host: localhost:' + port + '\r\n') + socket.write('Content-Type: text/plain\r\n') + socket.write('Content-Length: 0\r\n') + socket.write('Connection: keep-alive\r\n') + socket.write('\r\n') + }) + return socket +} From f6f8db95b2d6013c07fb7ee6041cf6b4b74fefd6 Mon Sep 17 00:00:00 2001 From: Martin Slota Date: Tue, 13 Aug 2024 14:25:43 +0200 Subject: [PATCH 2/3] Ensure stream gets closed and response gets ended when it is finished --- index.js | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/index.js b/index.js index 1d089427..0183bc99 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ var Buffer = require('safe-buffer').Buffer var bytes = require('bytes') var compressible = require('compressible') var debug = require('debug')('compression') +var onFinished = require('on-finished') var onHeaders = require('on-headers') var vary = require('vary') var zlib = require('zlib') @@ -110,6 +111,10 @@ function compression (options) { // mark ended ended = true + if (onFinished.isFinished(this)) { + return _end.call(this) + } + // write Buffer for Node.js 0.8 return chunk ? stream.end(toBuffer(chunk, encoding)) @@ -215,6 +220,12 @@ function compression (options) { _on.call(res, 'drain', function onResponseDrain () { stream.resume() }) + + onFinished(res, function onFinished () { + if (ended) { + _end.call(res) + } + }) }) next() diff --git a/package.json b/package.json index 41f27fb2..f0de480c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bytes": "3.0.0", "compressible": "~2.0.18", "debug": "2.6.9", + "on-finished": "~2.3.0", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" From aef3b535060450b6642f5c6d651eb48677a7978c Mon Sep 17 00:00:00 2001 From: Martin Slota Date: Tue, 13 Aug 2024 14:26:31 +0200 Subject: [PATCH 3/3] Only call the original res.end() a single time --- index.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 0183bc99..a92121c5 100644 --- a/index.js +++ b/index.js @@ -67,6 +67,14 @@ function compression (options) { var _on = res.on var _write = res.write + var endCalled = false + function endOnce () { + if (!endCalled) { + endCalled = true + _end.apply(this, arguments) + } + } + // flush res.flush = function flush () { if (stream) { @@ -105,14 +113,14 @@ function compression (options) { } if (!stream) { - return _end.call(this, chunk, encoding) + return endOnce.call(this, chunk, encoding) } // mark ended ended = true if (onFinished.isFinished(this)) { - return _end.call(this) + return endOnce.call(this) } // write Buffer for Node.js 0.8 @@ -214,7 +222,7 @@ function compression (options) { }) stream.on('end', function onStreamEnd () { - _end.call(res) + endOnce.call(res) }) _on.call(res, 'drain', function onResponseDrain () { @@ -223,7 +231,7 @@ function compression (options) { onFinished(res, function onFinished () { if (ended) { - _end.call(res) + endOnce.call(res) } }) })