Remove destroy, encoder, error-inject, escape-html, koa-is-json, on-finished, type-is and vary
This commit is contained in:
parent
6b27b844ff
commit
d655f208cb
9 changed files with 391 additions and 38 deletions
|
@ -6,10 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const debug = require('debug-ms')('koa:application');
|
const debug = require('debug-ms')('koa:application');
|
||||||
const onFinished = require('on-finished');
|
const onFinished = require('./onfinish');
|
||||||
const response = require('./response');
|
const response = require('./response');
|
||||||
const compose = require('koa-compose');
|
const compose = require('koa-compose');
|
||||||
const isJSON = require('koa-is-json');
|
const isJSON = require('./isjson');
|
||||||
const context = require('./context');
|
const context = require('./context');
|
||||||
const request = require('./request');
|
const request = require('./request');
|
||||||
const statuses = require('./statuses');
|
const statuses = require('./statuses');
|
||||||
|
|
13
lib/isjson.js
Normal file
13
lib/isjson.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Check if `body` should be interpreted as json.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isJSON(body) {
|
||||||
|
if (!body) return false;
|
||||||
|
if ('string' == typeof body) return false;
|
||||||
|
if ('function' == typeof body.pipe) return false;
|
||||||
|
if (Buffer.isBuffer(body)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = isJSON;
|
74
lib/onfinish.js
Normal file
74
lib/onfinish.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Call callback when request finished. Lifted off of
|
||||||
|
* npm on-finished with slight optimizations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function onFinished(msg, callback) {
|
||||||
|
let alreadyFinished = false;
|
||||||
|
|
||||||
|
// Make sure it hasn't finished already.
|
||||||
|
// Although I highly doubt this code is necessary.
|
||||||
|
if (typeof msg.finished === 'boolean') {
|
||||||
|
alreadyFinished = msg.finished || (msg.socket && !msg.socket.writable);
|
||||||
|
} else if (typeof msg.complete === 'boolean') {
|
||||||
|
alreadyFinished = msg.upgrade || !msg.socket || !msg.socket.readable || (msg.complete && !msg.readable);
|
||||||
|
} else {
|
||||||
|
// We don't support this object so end immediately
|
||||||
|
alreadyFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alreadyFinished) {
|
||||||
|
return setImmediate(callback, null, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.__onFinished) {
|
||||||
|
return msg.__onFinished.push(callback);
|
||||||
|
}
|
||||||
|
msg.__onFinished = [callback];
|
||||||
|
|
||||||
|
let socket = null;
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
function onFinish(error) {
|
||||||
|
if (finished) return;
|
||||||
|
|
||||||
|
msg.removeListener('end', onFinish);
|
||||||
|
msg.removeListener('finish', onFinish);
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.removeListener('error', onFinish);
|
||||||
|
socket.removeListener('close', onFinish);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = null;
|
||||||
|
finished = true;
|
||||||
|
|
||||||
|
msg.__onFinished.forEach(cb => cb(error, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.on('end', onFinish);
|
||||||
|
msg.on('finish', onFinish);
|
||||||
|
|
||||||
|
function onSocket(newSocket) {
|
||||||
|
// remove listener
|
||||||
|
msg.removeListener('socket', onSocket);
|
||||||
|
|
||||||
|
if (finished) return;
|
||||||
|
if (socket) return;
|
||||||
|
|
||||||
|
socket = newSocket;
|
||||||
|
|
||||||
|
// finished on first socket event
|
||||||
|
socket.on('error', onFinish);
|
||||||
|
socket.on('close', onFinish);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.socket) {
|
||||||
|
// socket already assigned
|
||||||
|
onSocket(msg.socket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for socket to be assigned
|
||||||
|
msg.on('socket', onSocket);
|
||||||
|
};
|
|
@ -5,19 +5,15 @@
|
||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const ReadStream = require('fs').ReadStream;
|
||||||
const contentDisposition = require('content-disposition');
|
const contentDisposition = require('content-disposition');
|
||||||
const ensureErrorHandler = require('error-inject');
|
const onFinish = require('./onfinish');
|
||||||
const onFinish = require('on-finished');
|
const isJSON = require('./isjson');
|
||||||
const isJSON = require('koa-is-json');
|
|
||||||
const escape = require('escape-html');
|
|
||||||
const typeis = require('type-is').is;
|
const typeis = require('type-is').is;
|
||||||
const statuses = require('./statuses');
|
const statuses = require('./statuses');
|
||||||
const destroy = require('destroy');
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const extname = require('path').extname;
|
const extname = require('path').extname;
|
||||||
const vary = require('vary');
|
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const encodeUrl = require('encodeurl');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prototype.
|
* Prototype.
|
||||||
|
@ -165,8 +161,25 @@ module.exports = {
|
||||||
|
|
||||||
// stream
|
// stream
|
||||||
if ('function' == typeof val.pipe) {
|
if ('function' == typeof val.pipe) {
|
||||||
onFinish(this.res, destroy.bind(null, val));
|
// On finish, destroy the stream
|
||||||
ensureErrorHandler(val, err => this.ctx.onerror(err));
|
onFinish(this.res, () => {
|
||||||
|
// Functionality taken from destroy
|
||||||
|
if (!(val instanceof ReadStream)) {
|
||||||
|
if (typeof val.destroy === 'function') {
|
||||||
|
val.destroy();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof val.close !== 'function') return;
|
||||||
|
|
||||||
|
// Fix potential bug (?) with node leaving file descriptor open
|
||||||
|
val.on('open', function() {
|
||||||
|
if (typeof this.fd === 'number') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
val.on('error', err => this.ctx.onerror(err));
|
||||||
|
|
||||||
// overwriting
|
// overwriting
|
||||||
if (null != original && original != val) this.remove('Content-Length');
|
if (null != original && original != val) this.remove('Content-Length');
|
||||||
|
@ -234,7 +247,14 @@ module.exports = {
|
||||||
vary(field) {
|
vary(field) {
|
||||||
if (this.headerSent) return;
|
if (this.headerSent) return;
|
||||||
|
|
||||||
vary(this.res, field);
|
// Revert #291, no reason to include full module
|
||||||
|
// that can be accomplished in 4 extra lines of code
|
||||||
|
let list = this.header.vary;
|
||||||
|
if (!list) return this.set('vary', field);
|
||||||
|
|
||||||
|
list = list.split(/ *, */);
|
||||||
|
if (!~list.indexOf(field)) list.push(field);
|
||||||
|
this.set('vary', list.join(', '));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -259,14 +279,18 @@ module.exports = {
|
||||||
redirect(url, alt) {
|
redirect(url, alt) {
|
||||||
// location
|
// location
|
||||||
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
|
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
|
||||||
this.set('Location', encodeUrl(url));
|
this.set('Location', encodeURI(url));
|
||||||
|
|
||||||
// status
|
// status
|
||||||
if (!statuses.redirect[this.status]) this.status = 302;
|
if (!statuses.redirect[this.status]) this.status = 302;
|
||||||
|
|
||||||
// html
|
// html
|
||||||
if (this.ctx.headers.accept && this.ctx.headers.accept.indexOf('html') >= 0) {
|
if (this.ctx.headers.accept && this.ctx.headers.accept.indexOf('html') >= 0) {
|
||||||
url = escape(url);
|
url = url.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
this.type = 'text/html; charset=utf-8';
|
this.type = 'text/html; charset=utf-8';
|
||||||
this.body = `Redirecting to <a href="${url}">${url}</a>.`;
|
this.body = `Redirecting to <a href="${url}">${url}</a>.`;
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -24,17 +24,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-disposition": "jharrilim/content-disposition#572383f",
|
"content-disposition": "jharrilim/content-disposition#572383f",
|
||||||
"debug-ms": "~4.1.2",
|
"debug-ms": "~4.1.2",
|
||||||
"destroy": "^1.0.4",
|
|
||||||
"encodeurl": "^1.0.2",
|
|
||||||
"error-inject": "^1.0.0",
|
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"fresh": "~0.5.2",
|
"fresh": "~0.5.2",
|
||||||
"http-errors-lite": "^2.0.2",
|
"http-errors-lite": "^2.0.2",
|
||||||
"koa-compose": "^4.1.0",
|
"koa-compose": "^4.1.0",
|
||||||
"koa-is-json": "^1.0.0",
|
"type-is": "^1.6.16"
|
||||||
"on-finished": "^2.3.0",
|
|
||||||
"type-is": "^1.6.16",
|
|
||||||
"vary": "^1.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"egg-bin": "^4.13.0",
|
"egg-bin": "^4.13.0",
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const context = require('../helpers/context');
|
const context = require('../helpers/context');
|
||||||
const parseurl = require('parseurl');
|
|
||||||
|
|
||||||
describe('ctx.path', () => {
|
describe('ctx.path', () => {
|
||||||
it('should return the pathname', () => {
|
it('should return the pathname', () => {
|
||||||
|
@ -30,11 +29,4 @@ describe('ctx.path=', () => {
|
||||||
assert.equal(ctx.originalUrl, '/login');
|
assert.equal(ctx.originalUrl, '/login');
|
||||||
assert.equal(ctx.request.originalUrl, '/login');
|
assert.equal(ctx.request.originalUrl, '/login');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect parseurl', () => {
|
|
||||||
const ctx = context({ url: '/login?foo=bar' });
|
|
||||||
ctx.path = '/login';
|
|
||||||
const url = parseurl(ctx.req);
|
|
||||||
assert.equal(url.path, '/login?foo=bar');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const context = require('../helpers/context');
|
const context = require('../helpers/context');
|
||||||
const parseurl = require('parseurl');
|
|
||||||
|
|
||||||
describe('ctx.querystring', () => {
|
describe('ctx.querystring', () => {
|
||||||
it('should return the querystring', () => {
|
it('should return the querystring', () => {
|
||||||
|
@ -44,11 +43,4 @@ describe('ctx.querystring=', () => {
|
||||||
assert.equal(ctx.originalUrl, '/store/shoes');
|
assert.equal(ctx.originalUrl, '/store/shoes');
|
||||||
assert.equal(ctx.request.originalUrl, '/store/shoes');
|
assert.equal(ctx.request.originalUrl, '/store/shoes');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect parseurl', () => {
|
|
||||||
const ctx = context({ url: '/login?foo=bar' });
|
|
||||||
ctx.querystring = 'foo=bar';
|
|
||||||
const url = parseurl(ctx.req);
|
|
||||||
assert.equal(url.path, '/login?foo=bar');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
265
test/utils/onfinish.js
Normal file
265
test/utils/onfinish.js
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
const http = require('http');
|
||||||
|
const net = require('net');
|
||||||
|
const onFinished = require('../../lib/onfinish');
|
||||||
|
|
||||||
|
describe('onFinished(res, listener)', () => {
|
||||||
|
it('should invoke listener given an unknown object', done => {
|
||||||
|
onFinished({}, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the response finishes', () => {
|
||||||
|
it('should fire the callback', done => {
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, done);
|
||||||
|
setTimeout(res.end.bind(res), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendGet(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include the response object', done => {
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, (err, msg) => {
|
||||||
|
assert.ok(!err);
|
||||||
|
assert.strictEqual(msg, res);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
setTimeout(res.end.bind(res), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendGet(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire when called after finish', done => {
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, () => {
|
||||||
|
onFinished(res, done);
|
||||||
|
});
|
||||||
|
setTimeout(res.end.bind(res), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendGet(server);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using keep-alive', () => {
|
||||||
|
it('should fire for each response', done => {
|
||||||
|
let called = false;
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, () => {
|
||||||
|
if (called) {
|
||||||
|
socket.end();
|
||||||
|
server.close();
|
||||||
|
done(called !== req ? null : new Error('fired twice on same req'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
called = req;
|
||||||
|
|
||||||
|
writeRequest(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
server.listen(function(){
|
||||||
|
socket = net.connect(this.address().port, function(){
|
||||||
|
writeRequest(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when requests pipelined', () => {
|
||||||
|
it('should fire for each request', done => {
|
||||||
|
let count = 0;
|
||||||
|
let responses = [];
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
responses.push(res);
|
||||||
|
|
||||||
|
onFinished(res, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.strictEqual(responses[0], res);
|
||||||
|
responses.shift();
|
||||||
|
|
||||||
|
if (responses.length === 0) {
|
||||||
|
socket.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responses[0].end('response b');
|
||||||
|
});
|
||||||
|
|
||||||
|
onFinished(req, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
if (++count !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(responses.length, 2);
|
||||||
|
responses[0].end('response a');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responses.length === 1) {
|
||||||
|
// second request
|
||||||
|
writeRequest(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.resume();
|
||||||
|
});
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
server.listen(function(){
|
||||||
|
let data = '';
|
||||||
|
socket = net.connect(this.address().port, function(){
|
||||||
|
writeRequest(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('data', chunk => {
|
||||||
|
data += chunk.toString('binary');
|
||||||
|
});
|
||||||
|
socket.on('end', () => {
|
||||||
|
assert.ok(/response a/.test(data));
|
||||||
|
assert.ok(/response b/.test(data));
|
||||||
|
server.close(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when response errors', () => {
|
||||||
|
it('should fire with error', done => {
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, err => {
|
||||||
|
assert.ok(err);
|
||||||
|
server.close(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', noop);
|
||||||
|
socket.write('W');
|
||||||
|
});
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
server.listen(function(){
|
||||||
|
socket = net.connect(this.address().port, function(){
|
||||||
|
writeRequest(this, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include the response object', done => {
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, (err, msg) => {
|
||||||
|
assert.ok(err);
|
||||||
|
assert.strictEqual(msg, res);
|
||||||
|
server.close(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', noop);
|
||||||
|
socket.write('W');
|
||||||
|
});
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
server.listen(function(){
|
||||||
|
socket = net.connect(this.address().port, function(){
|
||||||
|
writeRequest(this, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the response aborts', () => {
|
||||||
|
it('should execute the callback', done => {
|
||||||
|
let client;
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
onFinished(res, close(server, done));
|
||||||
|
setTimeout(client.abort.bind(client), 0);
|
||||||
|
});
|
||||||
|
server.listen(function(){
|
||||||
|
let port = this.address().port;
|
||||||
|
client = http.get('http://127.0.0.1:' + port);
|
||||||
|
client.on('error', noop);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when calling many times on same response', () => {
|
||||||
|
it('should not print warnings', done => {
|
||||||
|
let server = http.createServer((req, res) => {
|
||||||
|
let stderr = captureStderr(() => {
|
||||||
|
for (let i = 0; i < 400; i++) {
|
||||||
|
onFinished(res, noop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onFinished(res, done);
|
||||||
|
assert.strictEqual(stderr, '');
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(function(){
|
||||||
|
let port = this.address().port;
|
||||||
|
http.get('http://127.0.0.1:' + port, res => {
|
||||||
|
res.resume();
|
||||||
|
res.on('end', server.close.bind(server));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**********************************************************
|
||||||
|
* Removed request tests as those are not needed by our app
|
||||||
|
***********************************************************/
|
||||||
|
|
||||||
|
function captureStderr(fn){
|
||||||
|
let chunks = [];
|
||||||
|
let write = process.stderr.write;
|
||||||
|
|
||||||
|
process.stderr.write = function write(chunk, encoding){
|
||||||
|
chunks.push(new Buffer(chunk, encoding));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} finally {
|
||||||
|
process.stderr.write = write;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks).toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(server, callback){
|
||||||
|
return function(error){
|
||||||
|
server.close(err => {
|
||||||
|
callback(error || err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop(){}
|
||||||
|
|
||||||
|
function sendGet(server){
|
||||||
|
server.listen(function onListening(){
|
||||||
|
let port = this.address().port;
|
||||||
|
http.get('http://127.0.0.1:' + port, res => {
|
||||||
|
res.resume();
|
||||||
|
res.on('end', server.close.bind(server));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRequest(socket, chunked){
|
||||||
|
socket.write('GET / HTTP/1.1\r\n');
|
||||||
|
socket.write('Host: localhost\r\n');
|
||||||
|
socket.write('Connection: keep-alive\r\n');
|
||||||
|
|
||||||
|
if (chunked) {
|
||||||
|
socket.write('Transfer-Encoding: chunked\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.write('\r\n');
|
||||||
|
}
|
Loading…
Reference in a new issue