Spaces:
Sleeping
Sleeping
; | |
/** | |
* Module dependencies. | |
*/ | |
const isGeneratorFunction = require('is-generator-function'); | |
const debug = require('debug')('koa:application'); | |
const onFinished = require('on-finished'); | |
const assert = require('assert'); | |
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 only = require('only'); | |
const convert = require('koa-convert'); | |
const deprecate = require('depd')('koa'); | |
const { HttpError } = require('http-errors'); | |
/** | |
* 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) | |
* | |
*/ | |
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'; | |
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 {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 {Function} fn | |
* @return {Application} self | |
* @api public | |
*/ | |
use(fn) { | |
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); | |
if (isGeneratorFunction(fn)) { | |
deprecate('Support for generators will be removed in v3. ' + | |
'See the documentation for examples of how to convert old middleware ' + | |
'https://github.com/koajs/koa/blob/master/docs/migration.md'); | |
fn = convert(fn); | |
} | |
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 = 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 currnect contenxt 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) { | |
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.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 (404 === err.status || 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; | |
} | |
createAsyncCtxStorageMiddleware() { | |
const app = this; | |
return async function asyncCtxStorage(ctx, next) { | |
await app.ctxStorage.run(ctx, async() => { | |
return await next(); | |
}); | |
}; | |
} | |
}; | |
/** | |
* Response helper. | |
*/ | |
function respond(ctx) { | |
// allow bypassing koa | |
if (false === ctx.respond) return; | |
if (!ctx.writable) return; | |
const res = ctx.res; | |
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 (!res.headersSent && !ctx.response.has('Content-Length')) { | |
const { length } = ctx.response; | |
if (Number.isInteger(length)) ctx.length = length; | |
} | |
return res.end(); | |
} | |
// status body | |
if (null == body) { | |
if (ctx.response._explicitNullBody) { | |
ctx.response.remove('Content-Type'); | |
ctx.response.remove('Transfer-Encoding'); | |
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 ('string' === typeof body) return res.end(body); | |
if (body instanceof Stream) 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; | |