ebb4850709
closes #558 closes #557 Change tests to use plain functions and promises Add test return promise in middleware Change benchmarks to use plain functions and promises typeerror
211 lines
4.7 KiB
JavaScript
211 lines
4.7 KiB
JavaScript
|
|
'use strict';
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
const isGeneratorFunction = require('is-generator-function');
|
|
const debug = require('debug')('koa:application');
|
|
const Emitter = require('events');
|
|
const onFinished = require('on-finished');
|
|
const response = require('./response');
|
|
const compose = require('koa-compose');
|
|
const isJSON = require('koa-is-json');
|
|
const context = require('./context');
|
|
const request = require('./request');
|
|
const statuses = require('statuses');
|
|
const Cookies = require('cookies');
|
|
const accepts = require('accepts');
|
|
const assert = require('assert');
|
|
const Stream = require('stream');
|
|
const http = require('http');
|
|
const only = require('only');
|
|
|
|
/**
|
|
* Expose `Application` class.
|
|
* Inherits from `Emitter.prototype`.
|
|
*/
|
|
|
|
module.exports = class Application extends Emitter {
|
|
|
|
/**
|
|
* Initialize a new `Application`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.env = process.env.NODE_ENV || 'development';
|
|
this.subdomainOffset = 2;
|
|
this.middleware = [];
|
|
this.context = Object.create(context);
|
|
this.request = Object.create(request);
|
|
this.response = Object.create(response);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for:
|
|
*
|
|
* http.createServer(app.callback()).listen(...)
|
|
*
|
|
* @param {Mixed} ...
|
|
* @return {Server}
|
|
* @api public
|
|
*/
|
|
|
|
listen() {
|
|
debug('listen');
|
|
const server = http.createServer(this.callback());
|
|
return server.listen.apply(server, arguments);
|
|
}
|
|
|
|
/**
|
|
* Return JSON representation.
|
|
* We only bother showing settings.
|
|
*
|
|
* @return {Object}
|
|
* @api public
|
|
*/
|
|
|
|
toJSON() {
|
|
return only(this, [
|
|
'subdomainOffset',
|
|
'env'
|
|
]);
|
|
}
|
|
|
|
inspect() {
|
|
return this.toJSON();
|
|
}
|
|
|
|
/**
|
|
* Use the given middleware `fn`.
|
|
*
|
|
* @param {GeneratorFunction} fn
|
|
* @return {Application} self
|
|
* @api public
|
|
*/
|
|
|
|
use(fn) {
|
|
debug('use %s', fn._name || fn.name || '-');
|
|
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
|
|
if (isGeneratorFunction(fn)) {
|
|
throw new TypeError('Support for generators has been removed. Use Promises or wrap your generator with co.wrap');
|
|
}
|
|
this.middleware.push(fn);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Return a request handler callback
|
|
* for node's native http server.
|
|
*
|
|
* @return {Function}
|
|
* @api public
|
|
*/
|
|
|
|
callback() {
|
|
const fn = compose(this.middleware);
|
|
|
|
if (!this.listeners('error').length) this.on('error', this.onerror);
|
|
|
|
return (req, res) => {
|
|
res.statusCode = 404;
|
|
const ctx = this.createContext(req, res);
|
|
onFinished(res, ctx.onerror);
|
|
fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize a new context.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
createContext(req, res) {
|
|
const context = Object.create(this.context);
|
|
const request = context.request = Object.create(this.request);
|
|
const response = context.response = Object.create(this.response);
|
|
context.app = request.app = response.app = this;
|
|
context.req = request.req = response.req = req;
|
|
context.res = request.res = response.res = res;
|
|
request.ctx = response.ctx = context;
|
|
request.response = response;
|
|
response.request = request;
|
|
context.onerror = context.onerror.bind(context);
|
|
context.originalUrl = request.originalUrl = req.url;
|
|
context.cookies = new Cookies(req, res, this.keys);
|
|
context.accept = request.accept = accepts(req);
|
|
context.state = {};
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Default error handler.
|
|
*
|
|
* @param {Error} err
|
|
* @api private
|
|
*/
|
|
|
|
onerror(err) {
|
|
assert(err instanceof Error, `non-error thrown: ${err}`);
|
|
|
|
if (404 == err.status || err.expose) return;
|
|
if (this.silent) return;
|
|
|
|
const msg = err.stack || err.toString();
|
|
console.error();
|
|
console.error(msg.replace(/^/gm, ' '));
|
|
console.error();
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Response helper.
|
|
*/
|
|
|
|
function respond(ctx) {
|
|
// allow bypassing koa
|
|
if (false === ctx.respond) return;
|
|
|
|
const res = ctx.res;
|
|
if (res.headersSent || !ctx.writable) return;
|
|
|
|
let body = ctx.body;
|
|
const code = ctx.status;
|
|
|
|
// ignore body
|
|
if (statuses.empty[code]) {
|
|
// strip headers
|
|
ctx.body = null;
|
|
return res.end();
|
|
}
|
|
|
|
if ('HEAD' == ctx.method) {
|
|
if (isJSON(body)) ctx.length = Buffer.byteLength(JSON.stringify(body));
|
|
return res.end();
|
|
}
|
|
|
|
// status body
|
|
if (null == body) {
|
|
ctx.type = 'text';
|
|
body = ctx.message || String(code);
|
|
ctx.length = Buffer.byteLength(body);
|
|
return res.end(body);
|
|
}
|
|
|
|
// responses
|
|
if (Buffer.isBuffer(body)) return res.end(body);
|
|
if ('string' == typeof body) return res.end(body);
|
|
if (body instanceof Stream) return body.pipe(res);
|
|
|
|
// body: json
|
|
body = JSON.stringify(body);
|
|
ctx.length = Buffer.byteLength(body);
|
|
res.end(body);
|
|
}
|