application.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. 'use strict';
  2. /**
  3. * Module dependencies.
  4. */
  5. const isGeneratorFunction = require('is-generator-function');
  6. const debug = require('debug')('koa:application');
  7. const onFinished = require('on-finished');
  8. const response = require('./response');
  9. const compose = require('koa-compose');
  10. const context = require('./context');
  11. const request = require('./request');
  12. const statuses = require('statuses');
  13. const Emitter = require('events');
  14. const util = require('util');
  15. const Stream = require('stream');
  16. const http = require('http');
  17. const only = require('only');
  18. const convert = require('koa-convert');
  19. const deprecate = require('depd')('koa');
  20. const { HttpError } = require('http-errors');
  21. /**
  22. * Expose `Application` class.
  23. * Inherits from `Emitter.prototype`.
  24. */
  25. module.exports = class Application extends Emitter {
  26. /**
  27. * Initialize a new `Application`.
  28. *
  29. * @api public
  30. */
  31. /**
  32. *
  33. * @param {object} [options] Application options
  34. * @param {string} [options.env='development'] Environment
  35. * @param {string[]} [options.keys] Signed cookie keys
  36. * @param {boolean} [options.proxy] Trust proxy headers
  37. * @param {number} [options.subdomainOffset] Subdomain offset
  38. * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
  39. * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
  40. *
  41. */
  42. constructor(options) {
  43. super();
  44. options = options || {};
  45. this.proxy = options.proxy || false;
  46. this.subdomainOffset = options.subdomainOffset || 2;
  47. this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
  48. this.maxIpsCount = options.maxIpsCount || 0;
  49. this.env = options.env || process.env.NODE_ENV || 'development';
  50. if (options.keys) this.keys = options.keys;
  51. this.middleware = [];
  52. this.context = Object.create(context);
  53. this.request = Object.create(request);
  54. this.response = Object.create(response);
  55. // util.inspect.custom support for node 6+
  56. /* istanbul ignore else */
  57. if (util.inspect.custom) {
  58. this[util.inspect.custom] = this.inspect;
  59. }
  60. }
  61. /**
  62. * Shorthand for:
  63. *
  64. * http.createServer(app.callback()).listen(...)
  65. *
  66. * @param {Mixed} ...
  67. * @return {Server}
  68. * @api public
  69. */
  70. listen(...args) {
  71. debug('listen');
  72. const server = http.createServer(this.callback());
  73. return server.listen(...args);
  74. }
  75. /**
  76. * Return JSON representation.
  77. * We only bother showing settings.
  78. *
  79. * @return {Object}
  80. * @api public
  81. */
  82. toJSON() {
  83. return only(this, [
  84. 'subdomainOffset',
  85. 'proxy',
  86. 'env'
  87. ]);
  88. }
  89. /**
  90. * Inspect implementation.
  91. *
  92. * @return {Object}
  93. * @api public
  94. */
  95. inspect() {
  96. return this.toJSON();
  97. }
  98. /**
  99. * Use the given middleware `fn`.
  100. *
  101. * Old-style middleware will be converted.
  102. *
  103. * @param {Function} fn
  104. * @return {Application} self
  105. * @api public
  106. */
  107. use(fn) {
  108. if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  109. if (isGeneratorFunction(fn)) {
  110. deprecate('Support for generators will be removed in v3. ' +
  111. 'See the documentation for examples of how to convert old middleware ' +
  112. 'https://github.com/koajs/koa/blob/master/docs/migration.md');
  113. fn = convert(fn);
  114. }
  115. debug('use %s', fn._name || fn.name || '-');
  116. this.middleware.push(fn);
  117. return this;
  118. }
  119. /**
  120. * Return a request handler callback
  121. * for node's native http server.
  122. *
  123. * @return {Function}
  124. * @api public
  125. */
  126. callback() {
  127. const fn = compose(this.middleware);
  128. if (!this.listenerCount('error')) this.on('error', this.onerror);
  129. const handleRequest = (req, res) => {
  130. const ctx = this.createContext(req, res);
  131. return this.handleRequest(ctx, fn);
  132. };
  133. return handleRequest;
  134. }
  135. /**
  136. * Handle request in callback.
  137. *
  138. * @api private
  139. */
  140. handleRequest(ctx, fnMiddleware) {
  141. const res = ctx.res;
  142. res.statusCode = 404;
  143. const onerror = err => ctx.onerror(err);
  144. const handleResponse = () => respond(ctx);
  145. onFinished(res, onerror);
  146. return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  147. }
  148. /**
  149. * Initialize a new context.
  150. *
  151. * @api private
  152. */
  153. createContext(req, res) {
  154. const context = Object.create(this.context);
  155. const request = context.request = Object.create(this.request);
  156. const response = context.response = Object.create(this.response);
  157. context.app = request.app = response.app = this;
  158. context.req = request.req = response.req = req;
  159. context.res = request.res = response.res = res;
  160. request.ctx = response.ctx = context;
  161. request.response = response;
  162. response.request = request;
  163. context.originalUrl = request.originalUrl = req.url;
  164. context.state = {};
  165. return context;
  166. }
  167. /**
  168. * Default error handler.
  169. *
  170. * @param {Error} err
  171. * @api private
  172. */
  173. onerror(err) {
  174. // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
  175. // See https://github.com/koajs/koa/issues/1466
  176. // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
  177. const isNativeError =
  178. Object.prototype.toString.call(err) === '[object Error]' ||
  179. err instanceof Error;
  180. if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
  181. if (404 === err.status || err.expose) return;
  182. if (this.silent) return;
  183. const msg = err.stack || err.toString();
  184. console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
  185. }
  186. /**
  187. * Help TS users comply to CommonJS, ESM, bundler mismatch.
  188. * @see https://github.com/koajs/koa/issues/1513
  189. */
  190. static get default() {
  191. return Application;
  192. }
  193. };
  194. /**
  195. * Response helper.
  196. */
  197. function respond(ctx) {
  198. // allow bypassing koa
  199. if (false === ctx.respond) return;
  200. if (!ctx.writable) return;
  201. const res = ctx.res;
  202. let body = ctx.body;
  203. const code = ctx.status;
  204. // ignore body
  205. if (statuses.empty[code]) {
  206. // strip headers
  207. ctx.body = null;
  208. return res.end();
  209. }
  210. if ('HEAD' === ctx.method) {
  211. if (!res.headersSent && !ctx.response.has('Content-Length')) {
  212. const { length } = ctx.response;
  213. if (Number.isInteger(length)) ctx.length = length;
  214. }
  215. return res.end();
  216. }
  217. // status body
  218. if (null == body) {
  219. if (ctx.response._explicitNullBody) {
  220. ctx.response.remove('Content-Type');
  221. ctx.response.remove('Transfer-Encoding');
  222. return res.end();
  223. }
  224. if (ctx.req.httpVersionMajor >= 2) {
  225. body = String(code);
  226. } else {
  227. body = ctx.message || String(code);
  228. }
  229. if (!res.headersSent) {
  230. ctx.type = 'text';
  231. ctx.length = Buffer.byteLength(body);
  232. }
  233. return res.end(body);
  234. }
  235. // responses
  236. if (Buffer.isBuffer(body)) return res.end(body);
  237. if ('string' === typeof body) return res.end(body);
  238. if (body instanceof Stream) return body.pipe(res);
  239. // body: json
  240. body = JSON.stringify(body);
  241. if (!res.headersSent) {
  242. ctx.length = Buffer.byteLength(body);
  243. }
  244. res.end(body);
  245. }
  246. /**
  247. * Make HttpError available to consumers of the library so that consumers don't
  248. * have a direct dependency upon `http-errors`
  249. */
  250. module.exports.HttpError = HttpError;