UNPKG

koa

Version:

Koa web app framework

327 lines (280 loc) 8.48 kB
'use strict' /** * Module dependencies. */ const debug = require('debug')('koa:application') const assert = require('assert') const onFinished = require('on-finished') const response = require('./response') const compose = require('koa-compose') const context = require('./context') const request = require('./request') const statuses = require('statuses') const Emitter = require('events') const util = require('util') const Stream = require('stream') const http = require('http') const isStream = require('./is-stream.js') const only = require('./only.js') const { HttpError } = require('http-errors') /** @typedef {typeof import ('./context') & { * app: Application * req: import('http').IncomingMessage * res: import('http').ServerResponse * request: KoaRequest * response: KoaResponse * state: any * originalUrl: string * }} Context */ /** @typedef {typeof import('./request')} KoaRequest */ /** @typedef {typeof import('./response')} KoaResponse */ /** * Expose `Application` class. * Inherits from `Emitter.prototype`. */ module.exports = class Application extends Emitter { /** * Initialize a new `Application`. * * @api public */ /** * * @param {object} [options] Application options * @param {string} [options.env='development'] Environment * @param {string[]} [options.keys] Signed cookie keys * @param {boolean} [options.proxy] Trust proxy headers * @param {number} [options.subdomainOffset] Subdomain offset * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity) * @param {function} [options.compose] Function to handle middleware composition * @param {boolean} [options.asyncLocalStorage] Enable AsyncLocalStorage, default to false * */ constructor (options) { super() options = options || {} this.proxy = options.proxy || false this.subdomainOffset = options.subdomainOffset || 2 this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For' this.maxIpsCount = options.maxIpsCount || 0 this.env = options.env || process.env.NODE_ENV || 'development' this.compose = options.compose || compose if (options.keys) this.keys = options.keys this.middleware = [] this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response) // util.inspect.custom support for node 6+ /* istanbul ignore else */ if (util.inspect.custom) { this[util.inspect.custom] = this.inspect } if (options.asyncLocalStorage) { const { AsyncLocalStorage } = require('async_hooks') assert(AsyncLocalStorage, 'Requires node 12.17.0 or higher to enable asyncLocalStorage') this.ctxStorage = new AsyncLocalStorage() } } /** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {import('http').Server} * @api public */ listen (...args) { debug('listen') const server = http.createServer(this.callback()) return server.listen(...args) } /** * 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`. * * Old-style middleware will be converted. * * @param {(context: Context) => Promise<any | void>} fn * @return {Application} self * @api public */ use (fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') debug('use %s', fn._name || fn.name || '-') this.middleware.push(fn) return this } /** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ callback () { const fn = this.compose(this.middleware) if (!this.listenerCount('error')) this.on('error', this.onerror) const handleRequest = (req, res) => { const ctx = this.createContext(req, res) if (!this.ctxStorage) { return this.handleRequest(ctx, fn) } return this.ctxStorage.run(ctx, async () => { return await this.handleRequest(ctx, fn) }) } return handleRequest } /** * return current context from async local storage */ get currentContext () { if (this.ctxStorage) return this.ctxStorage.getStore() } /** * Handle request in callback. * * @api private */ handleRequest (ctx, fnMiddleware) { const res = ctx.res res.statusCode = 404 const onerror = err => ctx.onerror(err) const handleResponse = () => respond(ctx) onFinished(res, onerror) return fnMiddleware(ctx).then(handleResponse).catch(onerror) } /** * Initialize a new context. * * @api private */ createContext (req, res) { /** @type {Context} */ const context = Object.create(this.context) /** @type {KoaRequest} */ const request = context.request = Object.create(this.request) /** @type {KoaResponse} */ 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.originalUrl = request.originalUrl = req.url context.state = {} return context } /** * Default error handler. * * @param {Error} err * @api private */ onerror (err) { // When dealing with cross-globals a normal `instanceof` check doesn't work properly. // See https://github.com/koajs/koa/issues/1466 // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549. const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err)) if (err.status === 404 || err.expose) return if (this.silent) return const msg = err.stack || err.toString() console.error(`\n${msg.replace(/^/gm, ' ')}\n`) } /** * Help TS users comply to CommonJS, ESM, bundler mismatch. * @see https://github.com/koajs/koa/issues/1513 */ static get default () { return Application } } /** * Response helper. */ function respond (ctx) { // allow bypassing koa if (ctx.respond === false) return const res = ctx.res if (!ctx.writable) return res.end() let body = ctx.body const code = ctx.status // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null return res.end() } if (ctx.method === 'HEAD') { if (!res.headersSent && !ctx.response.has('Content-Length')) { const { length } = ctx.response if (Number.isInteger(length)) ctx.length = length } return res.end() } // status body if (body === null || body === undefined) { if (ctx.response._explicitNullBody) { ctx.response.remove('Content-Type') ctx.response.remove('Transfer-Encoding') ctx.length = 0 return res.end() } if (ctx.req.httpVersionMajor >= 2) { body = String(code) } else { body = ctx.message || String(code) } if (!res.headersSent) { ctx.type = 'text' ctx.length = Buffer.byteLength(body) } return res.end(body) } // responses if (Buffer.isBuffer(body)) return res.end(body) if (typeof body === 'string') return res.end(body) if (body instanceof Blob) return Stream.Readable.from(body.stream()).pipe(res) if (body instanceof ReadableStream) return Stream.Readable.from(body).pipe(res) if (body instanceof Response) return Stream.Readable.from(body?.body || '').pipe(res) if (isStream(body)) return body.pipe(res) // body: json body = JSON.stringify(body) if (!res.headersSent) { ctx.length = Buffer.byteLength(body) } res.end(body) } /** * Make HttpError available to consumers of the library so that consumers don't * have a direct dependency upon `http-errors` */ module.exports.HttpError = HttpError