Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy res.end(callback) calls correctly #751

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ function session(options) {
var _end = res.end;
var _write = res.write;
var ended = false;
res.end = function end(chunk, encoding) {
res.end = function end(chunk, encoding, callback) {
if (ended) {
return false;
}
Expand All @@ -257,14 +257,42 @@ function session(options) {
var ret;
var sync = true;

// Keep a reference to the original arguments so we can preserve their
// order when passing them on to the original _end()
var endArgs = arguments;

// The callback argument is optional. If provided, it may be the
// first, second, or third argument, and must be the last argument.
var callbackArgumentIndex = typeof chunk === 'function'
? 0
: typeof encoding === 'function'
? 1
: 2;

if (callbackArgumentIndex === 0) {
callback = chunk;
chunk = null;
encoding = null;
} else if (callbackArgumentIndex === 1) {
callback = encoding;
encoding = null;
}

function writeend() {
if (sync) {
ret = _end.call(res, chunk, encoding);
ret = _end.apply(res, endArgs);
sync = false;
return;
}

_end.call(res);
// This differs very slightly from (undocumented) Node behaviour in versions
// prior to 0.11.6 (which introduced callback support), because we explicitly
// call write() followed by end().
// The behaviour seems unintentional, as the callback is invoked immediately
// after write(), as opposed to after end().
var argumentsWithoutChunkOrEncoding = [null, null, null];
argumentsWithoutChunkOrEncoding[callbackArgumentIndex] = callback;
_end.apply(res, argumentsWithoutChunkOrEncoding);
}

function writetop() {
Expand All @@ -284,12 +312,17 @@ function session(options) {
chunk = !Buffer.isBuffer(chunk)
? Buffer.from(chunk, encoding)
: chunk;
endArgs[0] = chunk;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we pass endArgs to the original _end(), we need to keep it up to date. I believe the chunk is reassigned here because Node 0.8's end() only accepts a Buffer?

Mutating a function's arguments feels pretty bad, but it's by far the most elegant solution I came up with.

I'm not sure if this warrants a comment in the code.

encoding = undefined;
if (callbackArgumentIndex !== 1) {
endArgs[1] = encoding;
}

if (chunk.length !== 0) {
debug('split response');
ret = _write.call(res, chunk.slice(0, chunk.length - 1));
chunk = chunk.slice(chunk.length - 1, chunk.length);
endArgs[0] = chunk;
return ret;
}
}
Expand All @@ -309,7 +342,11 @@ function session(options) {
}

debug('destroyed');
writeend();
try {
writeend();
} catch (err) {
next(err);
}
});

return writetop();
Expand All @@ -318,7 +355,7 @@ function session(options) {
// no session to save
if (!req.session) {
debug('no session');
return _end.call(res, chunk, encoding);
return _end.apply(res, endArgs);
}

if (!touched) {
Expand All @@ -333,7 +370,11 @@ function session(options) {
defer(next, err);
}

writeend();
try {
writeend();
} catch (err) {
next(err);
}
});

return writetop();
Expand All @@ -346,13 +387,17 @@ function session(options) {
}

debug('touched');
writeend();
try {
writeend();
} catch (err) {
next(err);
}
});

return writetop();
}

return _end.call(res, chunk, encoding);
return _end.apply(res, endArgs);
};

// generate the session
Expand Down
88 changes: 88 additions & 0 deletions test/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,94 @@ describe('session()', function(){
.expect(200, 'Hello, world!', done);
})

describe('res.end() proxy', function () {
var nodeVersion = utils.getNodeVersion();

// Support for the callback argument was added in Node 0.11.6.
var nodeVersionSupportsResEndCallback = nodeVersion.major > 0 || nodeVersion.minor > 11 || (nodeVersion.minor === 11 && nodeVersion.patch >= 6);

it('should correctly handle callback as only argument', function (done) {
var callbackHasBeenCalled = false

var server = createServer(null, function (req, res) {
function callback() {
callbackHasBeenCalled = true
}

server.on('error', function onerror (err) {
assert.ok(err)
assert.strictEqual(err.message, 'first argument must be a string or Buffer')
done()
})

res.end(callback)
});

request(server)
.get('/')
.expect(200, '', function () {
if (nodeVersionSupportsResEndCallback) {
// If the Node version does not support the callback,
// the error listener will make the necessary assertions.
assert.ok(callbackHasBeenCalled)
done()
}
})
});

it('should correctly handle callback as second argument', function (done) {
var callbackHasBeenCalled = false

var server = createServer(null, function (req, res) {
function callback() {
callbackHasBeenCalled = true
}

res.end('hello', callback)
});

request(server).get('/').expect(200, 'hello', function () {
if (nodeVersionSupportsResEndCallback) {
assert.ok(callbackHasBeenCalled)
} else {
// This is (very slightly) different from the default Node behaviour,
// where the callback is invoked if `chunk` is a non-empty string or Buffer.
// This behaviour is undocumented, and seemingly happens unintentionally
// when the chunk and encoding are passed to write(), and in turn to
// afterWrite(), which considers the encoding argument to possibly be
// a function, and invokes it.
assert.strictEqual(callbackHasBeenCalled, false)
}

done()
})
})

it('should correctly handle callback as third argument', function (done) {
var callbackHasBeenCalled = false

var server = createServer(null, function (req, res) {
function callback() {
callbackHasBeenCalled = true
}

res.end('hello', 'utf8', callback)
});

request(server).get('/').expect(200, 'hello', function () {
// The third argument is completely ignored on Node versions
// that do not support the callback.
if (nodeVersionSupportsResEndCallback) {
assert.ok(callbackHasBeenCalled)
} else {
assert.strictEqual(callbackHasBeenCalled, false)
}

done()
})
})
})

it('should handle res.end(null) calls', function (done) {
var server = createServer(null, function (req, res) {
res.end(null)
Expand Down
11 changes: 11 additions & 0 deletions test/support/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module.exports.parseSetCookie = parseSetCookie
module.exports.writePatch = writePatch
module.exports.getNodeVersion = getNodeVersion

function parseSetCookie (header) {
var match
Expand Down Expand Up @@ -40,3 +41,13 @@ function writePatch (res) {
return _write.apply(this, arguments)
}
}

function getNodeVersion () {
var nodeVersionNumbers = process.versions.node.split('.').map(Number);

return {
major: nodeVersionNumbers[0],
minor: nodeVersionNumbers[1],
patch: nodeVersionNumbers[2]
}
}