application.js 8.0 KB

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