'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.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; 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', 'proxy', 'env' ]); } /** * Inspect implementation. * * @return {Object} * @api public */ 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); }