UNPKG

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