From bf94c75019473638d5be46cb80ff010a8a720054 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sat, 17 Dec 2016 13:09:30 -0500 Subject: [PATCH 01/10] Allow specifying file descriptor and use fs.open and fs.fstat Resolves #122 Resolves #123 --- README.md | 15 ++++++ index.js | 143 +++++++++++++++++++++++++++++++-------------------- test/send.js | 92 ++++++++++++++++++++++++++++----- 3 files changed, 181 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 0453578c..40166cdb 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ Enable or disable accepting ranged requests, defaults to true. Disabling this will not send `Accept-Ranges` and ignore the contents of the `Range` request header. +##### autoClose + +If `autoClose` is `false`, then the file descriptor won't be closed, +even if there's an error. It is your responsibility to close it and +make sure there's no file descriptor leak. If `autoClose` is set to +`true` (default behavior), on `error` or `end` the file descriptor +will be closed automatically. + ##### cacheControl Enable or disable setting `Cache-Control` response header, defaults to @@ -83,6 +91,11 @@ in the given order. By default, this is disabled (set to `false`). An example value that will serve extension-less HTML files: `['html', 'htm']`. This is skipped if the requested file already has an extension. +##### fd + +If `fd` is specified, send will ignore the `path` argument and will use the +specified file descriptor. This means that no `'open'` event will be emitted. + ##### index By default send supports "index.html" files, to disable this @@ -116,9 +129,11 @@ The `SendStream` is an event emitter and will emit the following events: - `error` an error occurred `(err)` - `directory` a directory was requested - `file` a file was requested `(path, stat)` + - `open` a file descriptor was opened for streaming `(fd)` - `headers` the headers are about to be set on a file `(res, path, stat)` - `stream` file streaming has started `(stream)` - `end` streaming has completed + - `close` the file descriptor was closed #### .pipe diff --git a/index.js b/index.js index 81ec0b3f..fa040747 100644 --- a/index.js +++ b/index.js @@ -166,9 +166,15 @@ function SendStream (req, path, options) { ? resolve(opts.root) : null + this.fd = typeof opts.fd === 'number' + ? opts.fd + : null + if (!this._root && opts.from) { this.from(opts.from) } + + this.onFileSystemError = this.onFileSystemError.bind(this) } /** @@ -374,7 +380,7 @@ SendStream.prototype.headersAlreadySent = function headersAlreadySent () { SendStream.prototype.isCachable = function isCachable () { var statusCode = this.res.statusCode return (statusCode >= 200 && statusCode < 300) || - statusCode === 304 + /* istanbul ignore next */ statusCode === 304 } /** @@ -384,7 +390,7 @@ SendStream.prototype.isCachable = function isCachable () { * @private */ -SendStream.prototype.onStatError = function onStatError (error) { +SendStream.prototype.onFileSystemError = function onFileSystemError (error) { switch (error.code) { case 'ENAMETOOLONG': case 'ENOENT': @@ -469,10 +475,33 @@ SendStream.prototype.redirect = function redirect (path) { SendStream.prototype.pipe = function pipe (res) { // root path var root = this._root + var self = this // references this.res = res + // response finished, done with the fd + onFinished(res, function onfinished () { + var fd = self.fd + var autoClose = self.options.autoClose + if (self._stream) destroy(self._stream) + if (typeof fd === 'number' && autoClose !== false) { + fs.close(fd, function () { + self.fd = null + self.emit('close') + }) + } + }) + + if (typeof this.fd === 'number') { + fs.fstat(this.fd, function (err, stat) { + if (err) return self.onFileSystemError(err) + self.emit('file', self.path, stat) + self.send(self.path, stat) + }) + return res + } + // decode the path var path = decode(this.path) if (path === -1) { @@ -573,7 +602,7 @@ SendStream.prototype.send = function send (path, stat) { return } - debug('pipe "%s"', path) + debug('pipe fd "%d" for path "%s"', this.fd, path) // set header fields this.setHeader(path, stat) @@ -664,34 +693,41 @@ SendStream.prototype.send = function send (path, stat) { SendStream.prototype.sendFile = function sendFile (path) { var i = 0 var self = this - - debug('stat "%s"', path) - fs.stat(path, function onstat (err, stat) { - if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) { - // not found, check extensions - return next(err) - } - if (err) return self.onStatError(err) - if (stat.isDirectory()) return self.redirect(self.path) - self.emit('file', path, stat) - self.send(path, stat) + var redirect = this.redirect.bind(this, this.path) + + debug('open "%s"', path) + fs.open(path, 'r', function onopen (err, fd) { + return !err + ? sendStats(path, fd, self.onFileSystemError, redirect) + : err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep + ? next(err) // not found, check extensions + : self.onFileSystemError(err) }) function next (err) { if (self._extensions.length <= i) { return err - ? self.onStatError(err) + ? self.onFileSystemError(err) : self.error(404) } - var p = path + '.' + self._extensions[i++] - - debug('stat "%s"', p) - fs.stat(p, function (err, stat) { + debug('open "%s"', p) + fs.open(p, 'r', function (err, fd) { if (err) return next(err) - if (stat.isDirectory()) return next() - self.emit('file', p, stat) - self.send(p, stat) + sendStats(p, fd, next, next) + }) + } + + function sendStats (path, fd, onError, onDirectory) { + debug('stat fd "%d" for path "%s"', fd, path) + fs.fstat(fd, function onstat (err, stat) { + if (err || stat.isDirectory()) return fs.close(fd, function () { /* istanbul ignore next */ + return err ? onError(err) : onDirectory() + }) + self.fd = fd + self.emit('file', path, stat) + self.emit('open', fd) + self.send(path, stat) }) } } @@ -702,28 +738,33 @@ SendStream.prototype.sendFile = function sendFile (path) { * @param {String} path * @api private */ + SendStream.prototype.sendIndex = function sendIndex (path) { var i = -1 var self = this - function next (err) { + return (function next (err) { if (++i >= self._index.length) { - if (err) return self.onStatError(err) + if (err) return self.onFileSystemError(err) return self.error(404) } var p = join(path, self._index[i]) - debug('stat "%s"', p) - fs.stat(p, function (err, stat) { + fs.open(p, 'r', function onopen (err, fd) { if (err) return next(err) - if (stat.isDirectory()) return next() - self.emit('file', p, stat) - self.send(p, stat) + debug('stat fd "%d" for path "%s"', fd, p) + fs.fstat(fd, function (err, stat) { + if (err || stat.isDirectory()) return fs.close(fd, function () { + next(err) + }) + self.fd = fd + self.emit('file', p, stat) + self.emit('open', fd) + self.send(p, stat) + }) }) - } - - next() + })() } /** @@ -735,39 +776,27 @@ SendStream.prototype.sendIndex = function sendIndex (path) { */ SendStream.prototype.stream = function stream (path, options) { - // TODO: this is all lame, refactor meeee - var finished = false - var self = this - var res = this.res - - // pipe - var stream = fs.createReadStream(path, options) - this.emit('stream', stream) - stream.pipe(res) - - // response finished, done with the fd - onFinished(res, function onfinished () { - finished = true - destroy(stream) - }) + options.fd = this.fd + options.autoClose = false - // error handling code-smell - stream.on('error', function onerror (err) { - // request already finished - if (finished) return - - // clean up stream - finished = true - destroy(stream) + var self = this + var stream = this._stream = fs.createReadStream(path, options) - // error - self.onStatError(err) + // error + stream.on('error', function onerror (e) { + stream.fd = null + self.onFileSystemError(e) }) // end stream.on('end', function onend () { + stream.fd = null self.emit('end') }) + + // pipe + this.emit('stream', stream) + stream.pipe(this.res) } /** diff --git a/test/send.js b/test/send.js index da101aa9..c7ba1d7d 100644 --- a/test/send.js +++ b/test/send.js @@ -24,6 +24,40 @@ var app = http.createServer(function (req, res) { .pipe(res) }) +var fsOpen = fs.open +var fsClose = fs.close +var fds = { + opened: 0, + closed: 0 +} + +before(function () { + fs.open = function () { + var args = Array.prototype.slice.call(arguments) + var last = args.length - 1 + var done = args[last] + args[last] = function (err, fd) { + if (typeof fd === 'number') fds.opened++ + done(err, fd) + } + return fsOpen.apply(fs, args) + } + fs.close = function (fd, cb) { + fds.closed++ + return fsClose.call(fs, fd, cb) + } +}) + +after(function () { + fs.open = fsOpen + fs.close = fsClose +}) + +afterEach(function () { + var reason = 'all opened file descriptors (' + fds.opened + ') should have been closed (' + fds.closed + ')' + assert.equal(fds.closed, fds.opened, reason) +}) + describe('send(file).pipe(res)', function () { it('should stream the file contents', function (done) { request(app) @@ -155,24 +189,25 @@ describe('send(file).pipe(res)', function () { }) }) - it('should 404 if file disappears after stat, before open', function (done) { + it('should 200 even if file disappears after stat', function (done) { + var resource = '/tmp.txt' + var tmpPath = path.join(__dirname, 'fixtures', resource) var app = http.createServer(function (req, res) { send(req, req.url, {root: 'test/fixtures'}) - .on('file', function () { - // simulate file ENOENT after on open, after stat - var fn = this.send - this.send = function (path, stat) { - path += '__xxx_no_exist' - fn.call(this, path, stat) - } + .on('file', function (path) { + fs.unlinkSync(tmpPath) }) .pipe(res) }) - request(app) - .get('/name.txt') - .expect('Content-Type', /plain/) - .expect(404, done) + fs.writeFile(tmpPath, 'howdy', { flag: 'wx' }, function (err) { + if (err) return done(err) + request(app) + .get(resource) + .expect('Content-Type', /plain/) + .expect(200, done) + }) + }) it('should 500 on file stream error', function (done) { @@ -873,6 +908,39 @@ describe('send(file, options)', function () { }) }) + describe('fd', function () { + it('should support providing an existing file descriptor', function (done) { + var resource = '/nums' + fs.open(path.join(fixtures, resource), 'r', function (err, fd) { + if (err) return done(err) + request(createServer({fd: fd})) + .get(resource) + .expect(200, done) + }) + }) + + it('should still error if the fd cannot be streamed', function (done) { + request(createServer({fd: 999, autoClose: false})) + .get('/anything') + .expect(500, done) + }) + }) + + describe('autoClose', function () { + it('should prevent the file descriptor from being closed automatically', function (done) { + var resource = '/nums' + fs.open(path.join(fixtures, resource), 'r', function (err, fd) { + if (err) return done(err) + request(createServer({fd: fd, autoClose: false})) + .get(resource) + .expect(200, function (err) { + if (err) return done(err) + fs.close(fd, done) + }) + }) + }) + }) + describe('lastModified', function () { it('should support disabling last-modified', function (done) { request(createServer({lastModified: false, root: fixtures})) From 59c716c4910bfb052f85144f705e424198ef7bbe Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sat, 17 Dec 2016 13:28:56 -0500 Subject: [PATCH 02/10] Fix linting issues --- index.js | 16 ++++++++++------ test/send.js | 1 - 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index fa040747..0c9213d4 100644 --- a/index.js +++ b/index.js @@ -721,9 +721,11 @@ SendStream.prototype.sendFile = function sendFile (path) { function sendStats (path, fd, onError, onDirectory) { debug('stat fd "%d" for path "%s"', fd, path) fs.fstat(fd, function onstat (err, stat) { - if (err || stat.isDirectory()) return fs.close(fd, function () { /* istanbul ignore next */ - return err ? onError(err) : onDirectory() - }) + if (err || stat.isDirectory()) { + return fs.close(fd, function () { /* istanbul ignore next */ + return err ? onError(err) : onDirectory() + }) + } self.fd = fd self.emit('file', path, stat) self.emit('open', fd) @@ -755,9 +757,11 @@ SendStream.prototype.sendIndex = function sendIndex (path) { if (err) return next(err) debug('stat fd "%d" for path "%s"', fd, p) fs.fstat(fd, function (err, stat) { - if (err || stat.isDirectory()) return fs.close(fd, function () { - next(err) - }) + if (err || stat.isDirectory()) { + return fs.close(fd, function () { + next(err) + }) + } self.fd = fd self.emit('file', p, stat) self.emit('open', fd) diff --git a/test/send.js b/test/send.js index c7ba1d7d..2e5031bc 100644 --- a/test/send.js +++ b/test/send.js @@ -207,7 +207,6 @@ describe('send(file).pipe(res)', function () { .expect('Content-Type', /plain/) .expect(200, done) }) - }) it('should 500 on file stream error', function (done) { From 78fd8b2b1d2083eab783e00e00eb6281cb4da0e2 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sun, 22 Jan 2017 11:17:04 -0500 Subject: [PATCH 03/10] Handle fs.close() errors --- index.js | 13 +++++++------ test/send.js | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 0c9213d4..daf0b7db 100644 --- a/index.js +++ b/index.js @@ -482,11 +482,12 @@ SendStream.prototype.pipe = function pipe (res) { // response finished, done with the fd onFinished(res, function onfinished () { - var fd = self.fd - var autoClose = self.options.autoClose + var autoClose = self.options.autoClose !== false if (self._stream) destroy(self._stream) - if (typeof fd === 'number' && autoClose !== false) { - fs.close(fd, function () { + if (typeof self.fd === 'number' && autoClose !== false) { + fs.close(self.fd, function (err) { + /* istanbul ignore next */ + if (err && err.code !== 'EBADF') return self.onFileSystemError(err) self.fd = null self.emit('close') }) @@ -758,8 +759,8 @@ SendStream.prototype.sendIndex = function sendIndex (path) { debug('stat fd "%d" for path "%s"', fd, p) fs.fstat(fd, function (err, stat) { if (err || stat.isDirectory()) { - return fs.close(fd, function () { - next(err) + return fs.close(fd, function (e) { + next(err || e) }) } self.fd = fd diff --git a/test/send.js b/test/send.js index 2e5031bc..44c60463 100644 --- a/test/send.js +++ b/test/send.js @@ -1,7 +1,7 @@ process.env.NO_DEPRECATION = 'send' -var after = require('after') +var afterCalls = require('after') // do not interfere with mocha's after() global var assert = require('assert') var fs = require('fs') var http = require('http') @@ -26,13 +26,16 @@ var app = http.createServer(function (req, res) { var fsOpen = fs.open var fsClose = fs.close -var fds = { - opened: 0, - closed: 0 -} - +var fds = { opened: 0, closed: 0 } +// Before any of the tests run wrap `fs.open()` and `fs.close()` +// so we can test that all the file descriptors that get opened +// by `send()` are also closed properly after each response before(function () { - fs.open = function () { + // When `fs.open()` is called we wrap the last argument (the + // callback which receives the file descriptor) so that when + // it is evuantually called we can increment the number of + // opened file descriptors + fs.open = function wrappedFSopen () { var args = Array.prototype.slice.call(arguments) var last = args.length - 1 var done = args[last] @@ -42,17 +45,25 @@ before(function () { } return fsOpen.apply(fs, args) } - fs.close = function (fd, cb) { - fds.closed++ + + // When `fs.close()` is called we increment the number of closed + // file descriptors immediately since closing is asynchronous and + // we check after each response that any opened file descriptors + // will eventually be closed, not necessarily that they *are* now + fs.close = function wrappedFSclose (fd, cb) { + if (typeof fd === 'number') fds.closed++ return fsClose.call(fs, fd, cb) } }) +// After all the tests have run then restore `fs.open()` +// and `fs.close()` to their original un-wrapped versions after(function () { fs.open = fsOpen fs.close = fsClose }) +// After each test: ensure all opened file descriptors have been closed afterEach(function () { var reason = 'all opened file descriptors (' + fds.opened + ') should have been closed (' + fds.closed + ')' assert.equal(fds.closed, fds.opened, reason) @@ -228,7 +239,7 @@ describe('send(file).pipe(res)', function () { describe('"headers" event', function () { it('should fire when sending file', function (done) { - var cb = after(2, done) + var cb = afterCalls(2, done) var server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', function () { cb() }) @@ -241,7 +252,7 @@ describe('send(file).pipe(res)', function () { }) it('should not fire on 404', function (done) { - var cb = after(1, done) + var cb = afterCalls(1, done) var server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', function () { cb() }) @@ -254,7 +265,7 @@ describe('send(file).pipe(res)', function () { }) it('should fire on index', function (done) { - var cb = after(2, done) + var cb = afterCalls(2, done) var server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', function () { cb() }) @@ -267,7 +278,7 @@ describe('send(file).pipe(res)', function () { }) it('should not fire on redirect', function (done) { - var cb = after(1, done) + var cb = afterCalls(1, done) var server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', function () { cb() }) @@ -280,7 +291,7 @@ describe('send(file).pipe(res)', function () { }) it('should provide path', function (done) { - var cb = after(2, done) + var cb = afterCalls(2, done) var server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', onHeaders) @@ -299,7 +310,7 @@ describe('send(file).pipe(res)', function () { }) it('should provide stat', function (done) { - var cb = after(2, done) + var cb = afterCalls(2, done) var server = http.createServer(function (req, res) { send(req, req.url, {root: fixtures}) .on('headers', onHeaders) From 31e9c839da41e470378a0d85305461e91d1219f2 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sun, 22 Jan 2017 11:19:07 -0500 Subject: [PATCH 04/10] Adjust comment --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index daf0b7db..a27812c3 100644 --- a/index.js +++ b/index.js @@ -384,7 +384,7 @@ SendStream.prototype.isCachable = function isCachable () { } /** - * Handle stat() error. + * Handle file system error. * * @param {Error} error * @private From 7ef9e1cbb50b06fb572ab7c6cd8691aece58f5c3 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sun, 22 Jan 2017 11:20:36 -0500 Subject: [PATCH 05/10] No need to test this twice --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index a27812c3..d3cc454e 100644 --- a/index.js +++ b/index.js @@ -484,7 +484,7 @@ SendStream.prototype.pipe = function pipe (res) { onFinished(res, function onfinished () { var autoClose = self.options.autoClose !== false if (self._stream) destroy(self._stream) - if (typeof self.fd === 'number' && autoClose !== false) { + if (typeof self.fd === 'number' && autoClose) { fs.close(self.fd, function (err) { /* istanbul ignore next */ if (err && err.code !== 'EBADF') return self.onFileSystemError(err) From 07999c7aa18b10309bc8cfac2cd344ee74117bd6 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Mon, 23 Jan 2017 09:32:15 -0500 Subject: [PATCH 06/10] Handle additional fs.close error --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index d3cc454e..2b408a38 100644 --- a/index.js +++ b/index.js @@ -723,8 +723,8 @@ SendStream.prototype.sendFile = function sendFile (path) { debug('stat fd "%d" for path "%s"', fd, path) fs.fstat(fd, function onstat (err, stat) { if (err || stat.isDirectory()) { - return fs.close(fd, function () { /* istanbul ignore next */ - return err ? onError(err) : onDirectory() + return fs.close(fd, function (e) { /* istanbul ignore next */ + return (err || e) ? onError(err || e) : onDirectory() }) } self.fd = fd From 5486e0d35b01a08d8cc7ea4fa228dd86958d4e87 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Mon, 23 Jan 2017 10:39:44 -0500 Subject: [PATCH 07/10] Create PartStream constructor to ensure the autoClose option is respected in node.js < 0.10. Also remove the `destroy` dependency. --- index.js | 34 ++++++++++++++++++---------------- package.json | 1 - 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 2b408a38..3a7ad9ad 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,6 @@ var createError = require('http-errors') var debug = require('debug')('send') var deprecate = require('depd')('send') -var destroy = require('destroy') var encodeUrl = require('encodeurl') var escapeHtml = require('escape-html') var etag = require('etag') @@ -483,7 +482,7 @@ SendStream.prototype.pipe = function pipe (res) { // response finished, done with the fd onFinished(res, function onfinished () { var autoClose = self.options.autoClose !== false - if (self._stream) destroy(self._stream) + if (self._stream) self._stream.destroy() if (typeof self.fd === 'number' && autoClose) { fs.close(self.fd, function (err) { /* istanbul ignore next */ @@ -682,7 +681,7 @@ SendStream.prototype.send = function send (path, stat) { return } - this.stream(path, opts) + this.stream(opts) } /** @@ -773,31 +772,23 @@ SendStream.prototype.sendIndex = function sendIndex (path) { } /** - * Stream `path` to the response. + * Stream to the response. * - * @param {String} path * @param {Object} options * @api private */ -SendStream.prototype.stream = function stream (path, options) { +SendStream.prototype.stream = function stream (options) { options.fd = this.fd options.autoClose = false - var self = this - var stream = this._stream = fs.createReadStream(path, options) + var stream = this._stream = new PartStream(options) // error - stream.on('error', function onerror (e) { - stream.fd = null - self.onFileSystemError(e) - }) + stream.on('error', this.onFileSystemError) // end - stream.on('end', function onend () { - stream.fd = null - self.emit('end') - }) + stream.on('end', this.emit.bind(this, 'end')) // pipe this.emit('stream', stream) @@ -868,6 +859,17 @@ SendStream.prototype.setHeader = function setHeader (path, stat) { } } +util.inherits(PartStream, fs.ReadStream) + +function PartStream (opts) { + fs.ReadStream.call(this, null, opts) + this.bufferSize = opts.highWaterMark || this.bufferSize +} + +PartStream.prototype.destroy = PartStream.prototype.close = function closePartStream () { + this.readable = !(this.destroyed = this.closed = !(this.fd = null)) +} + /** * Clear all headers from a response. * diff --git a/package.json b/package.json index c39ee915..5cf2168a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "dependencies": { "debug": "~2.2.0", "depd": "~1.1.0", - "destroy": "~1.0.4", "encodeurl": "~1.0.1", "escape-html": "~1.0.3", "etag": "~1.7.0", From ed3055fce3d1c812153feb2db408578c2db46346 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sat, 28 Jan 2017 12:58:17 -0500 Subject: [PATCH 08/10] Handle node/libuv error on windows where instead of returning an error code of `EBADF` it returns an error code of `OK` which makes zero sense --- index.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 3a7ad9ad..97b305ea 100644 --- a/index.js +++ b/index.js @@ -62,6 +62,18 @@ var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year var UP_PATH_REGEXP = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/ +/** + * Regular expression to match a bad file number error code + * Node on Windows incorrectly sets the error code to `OK` + * instead of `EBADF` because of a bug in libuv + * @type {RegExp} + * @const + * @see {@link https://github.com/nodejs/node/issues/3718} + * @see {@link https://github.com/libuv/libuv/pull/613} + */ + +var BAD_FD_REGEXP = /^(EBADF|OK)$/ + /** * Module exports. * @public @@ -486,7 +498,7 @@ SendStream.prototype.pipe = function pipe (res) { if (typeof self.fd === 'number' && autoClose) { fs.close(self.fd, function (err) { /* istanbul ignore next */ - if (err && err.code !== 'EBADF') return self.onFileSystemError(err) + if (err && !BAD_FD_REGEXP.test(err.code)) return self.onFileSystemError(err) self.fd = null self.emit('close') }) From ff711b0a3fbdd4adbf67d658c6583aa653d6aab2 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Wed, 1 Feb 2017 19:30:20 -0500 Subject: [PATCH 09/10] Ensure fd is closed in the following cases: 1. The `res` ends before it is passed to `send().pipe()` 2. The `res` ends after pipe but before the `open` event --- index.js | 53 +++++++++++++++++++++++++++++++++++++++------------- test/send.js | 36 ++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 97b305ea..5ec319ad 100644 --- a/index.js +++ b/index.js @@ -24,6 +24,7 @@ var fs = require('fs') var mime = require('mime') var ms = require('ms') var onFinished = require('on-finished') +var isFinished = onFinished.isFinished var parseRange = require('range-parser') var path = require('path') var statuses = require('statuses') @@ -181,6 +182,8 @@ function SendStream (req, path, options) { ? opts.fd : null + this.autoClose = opts.autoClose !== false + if (!this._root && opts.from) { this.from(opts.from) } @@ -476,7 +479,35 @@ SendStream.prototype.redirect = function redirect (path) { } /** - * Pipe to `res. + * End the stream. + * + * @private + */ + +SendStream.prototype.end = function end () { + this.send = this.close + if (this._stream) this._stream.destroy() + if (this.autoClose) this.close() +} + +/** + * Close the file descriptor. + * + * @private + */ + +SendStream.prototype.close = function close () { + if (typeof this.fd !== 'number') return + var self = this + fs.close(this.fd, function (err) { /* istanbul ignore next */ + if (err && !BAD_FD_REGEXP.test(err.code)) return self.onFileSystemError(err) + self.fd = null + self.emit('close') + }) +} + +/** + * Pipe to `res`. * * @param {Stream} res * @return {Stream} res @@ -492,18 +523,12 @@ SendStream.prototype.pipe = function pipe (res) { this.res = res // response finished, done with the fd - onFinished(res, function onfinished () { - var autoClose = self.options.autoClose !== false - if (self._stream) self._stream.destroy() - if (typeof self.fd === 'number' && autoClose) { - fs.close(self.fd, function (err) { - /* istanbul ignore next */ - if (err && !BAD_FD_REGEXP.test(err.code)) return self.onFileSystemError(err) - self.fd = null - self.emit('close') - }) - } - }) + if (isFinished(res)) { + this.end() + return res + } + + onFinished(res, this.end.bind(this)) if (typeof this.fd === 'number') { fs.fstat(this.fd, function (err, stat) { @@ -608,6 +633,8 @@ SendStream.prototype.send = function send (path, stat) { var ranges = req.headers.range var offset = options.start || 0 + if (isFinished(res)) return this.end() + if (res._header) { // impossible to send now this.headersAlreadySent() diff --git a/test/send.js b/test/send.js index 44c60463..a34772b4 100644 --- a/test/send.js +++ b/test/send.js @@ -24,6 +24,7 @@ var app = http.createServer(function (req, res) { .pipe(res) }) +var noop = Function.prototype var fsOpen = fs.open var fsClose = fs.close var fds = { opened: 0, closed: 0 } @@ -934,10 +935,43 @@ describe('send(file, options)', function () { .get('/anything') .expect(500, done) }) + + it('should close the file descriptor if the response ends before pipe', function (done) { + fs.open(path.join(fixtures, '/name.txt'), 'r', function (err, fd) { + if (err) return done(err) + var app = http.createServer(function (req, res) { + res.end() + send(req, req.url, req.url === '/fd' ? {fd: fd} : {root: 'test/fixtures'}) + .on('close', done) + .pipe(res) + }) + + request(app) + .get('/nums') + .expect(200, function () { + request(app) + .get('/fd') + .expect(200, noop) + }) + }) + }) + + it('should close the file descriptor if the response ends immediately after pipe', function (done) { + var app = http.createServer(function (req, res) { + send(req, req.url, {root: 'test/fixtures'}) + .on('close', done) + .pipe(res) + res.end() + }) + + request(app) + .get('/name.txt') + .expect(200, noop) + }) }) describe('autoClose', function () { - it('should prevent the file descriptor from being closed automatically', function (done) { + it('should prevent the file descriptor from being closed automatically if set to `false`', function (done) { var resource = '/nums' fs.open(path.join(fixtures, resource), 'r', function (err, fd) { if (err) return done(err) From 26a52a8b9cf80a13278151e2dca33f590bd6689e Mon Sep 17 00:00:00 2001 From: James Wyatt Cready Date: Sat, 11 Feb 2017 15:04:32 -0500 Subject: [PATCH 10/10] Fix typo, improve tests, clean up code --- index.js | 13 +++++-------- test/send.js | 32 +++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 5ec319ad..c7904ef3 100644 --- a/index.js +++ b/index.js @@ -608,7 +608,7 @@ SendStream.prototype.pipe = function pipe (res) { } // index file support - if (this._index.length && this.path[this.path.length - 1] === '/') { + if (this._index.length && this.hasTrailingSlash()) { this.sendIndex(path) return res } @@ -633,8 +633,6 @@ SendStream.prototype.send = function send (path, stat) { var ranges = req.headers.range var offset = options.start || 0 - if (isFinished(res)) return this.end() - if (res._header) { // impossible to send now this.headersAlreadySent() @@ -736,11 +734,10 @@ SendStream.prototype.sendFile = function sendFile (path) { debug('open "%s"', path) fs.open(path, 'r', function onopen (err, fd) { - return !err - ? sendStats(path, fd, self.onFileSystemError, redirect) - : err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep - ? next(err) // not found, check extensions - : self.onFileSystemError(err) + if (!err) return sendStats(path, fd, self.onFileSystemError, redirect) + return err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep + ? next(err) // not found, check extensions + : self.onFileSystemError(err) }) function next (err) { diff --git a/test/send.js b/test/send.js index a34772b4..4c17de6e 100644 --- a/test/send.js +++ b/test/send.js @@ -34,7 +34,7 @@ var fds = { opened: 0, closed: 0 } before(function () { // When `fs.open()` is called we wrap the last argument (the // callback which receives the file descriptor) so that when - // it is evuantually called we can increment the number of + // it is eventually called we can increment the number of // opened file descriptors fs.open = function wrappedFSopen () { var args = Array.prototype.slice.call(arguments) @@ -937,21 +937,31 @@ describe('send(file, options)', function () { }) it('should close the file descriptor if the response ends before pipe', function (done) { - fs.open(path.join(fixtures, '/name.txt'), 'r', function (err, fd) { - if (err) return done(err) - var app = http.createServer(function (req, res) { + var app = http.createServer(function (req, res) { + if (req.url === '/fd') { + fs.open(path.join(fixtures, '/name.txt'), 'r', function (err, fd) { + res.end() + if (err) return done(err) + send(req, req.url, {fd: fd}) + .on('close', done) + .pipe(res) + }) + } else { res.end() - send(req, req.url, req.url === '/fd' ? {fd: fd} : {root: 'test/fixtures'}) + send(req, req.url, {root: 'test/fixtures'}) .on('close', done) .pipe(res) - }) + } + }) + request(app) + .get('/nums') + .expect(200, function (err) { + if (err) return done(err) request(app) - .get('/nums') - .expect(200, function () { - request(app) - .get('/fd') - .expect(200, noop) + .get('/fd') + .expect(200, function (err) { + if (err) done(err) }) }) })