UNPKG

12.1 kBJavaScriptView Raw
1
2'use strict';
3
4/**
5 * Module dependencies.
6 */
7
8const contentDisposition = require('content-disposition');
9const ensureErrorHandler = require('error-inject');
10const getType = require('cache-content-type');
11const onFinish = require('on-finished');
12const escape = require('escape-html');
13const typeis = require('type-is').is;
14const statuses = require('statuses');
15const destroy = require('destroy');
16const assert = require('assert');
17const extname = require('path').extname;
18const vary = require('vary');
19const only = require('only');
20const util = require('util');
21const encodeUrl = require('encodeurl');
22const Stream = require('stream');
23
24/**
25 * Prototype.
26 */
27
28module.exports = {
29
30 /**
31 * Return the request socket.
32 *
33 * @return {Connection}
34 * @api public
35 */
36
37 get socket() {
38 return this.res.socket;
39 },
40
41 /**
42 * Return response header.
43 *
44 * @return {Object}
45 * @api public
46 */
47
48 get header() {
49 const { res } = this;
50 return typeof res.getHeaders === 'function'
51 ? res.getHeaders()
52 : res._headers || {}; // Node < 7.7
53 },
54
55 /**
56 * Return response header, alias as response.header
57 *
58 * @return {Object}
59 * @api public
60 */
61
62 get headers() {
63 return this.header;
64 },
65
66 /**
67 * Get response status code.
68 *
69 * @return {Number}
70 * @api public
71 */
72
73 get status() {
74 return this.res.statusCode;
75 },
76
77 /**
78 * Set response status code.
79 *
80 * @param {Number} code
81 * @api public
82 */
83
84 set status(code) {
85 if (this.headerSent) return;
86
87 assert(Number.isInteger(code), 'status code must be a number');
88 assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
89 this._explicitStatus = true;
90 this.res.statusCode = code;
91 if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
92 if (this.body && statuses.empty[code]) this.body = null;
93 },
94
95 /**
96 * Get response status message
97 *
98 * @return {String}
99 * @api public
100 */
101
102 get message() {
103 return this.res.statusMessage || statuses[this.status];
104 },
105
106 /**
107 * Set response status message
108 *
109 * @param {String} msg
110 * @api public
111 */
112
113 set message(msg) {
114 this.res.statusMessage = msg;
115 },
116
117 /**
118 * Get response body.
119 *
120 * @return {Mixed}
121 * @api public
122 */
123
124 get body() {
125 return this._body;
126 },
127
128 /**
129 * Set response body.
130 *
131 * @param {String|Buffer|Object|Stream} val
132 * @api public
133 */
134
135 set body(val) {
136 const original = this._body;
137 this._body = val;
138
139 // no content
140 if (null == val) {
141 if (!statuses.empty[this.status]) this.status = 204;
142 this.remove('Content-Type');
143 this.remove('Content-Length');
144 this.remove('Transfer-Encoding');
145 return;
146 }
147
148 // set the status
149 if (!this._explicitStatus) this.status = 200;
150
151 // set the content-type only if not yet set
152 const setType = !this.has('Content-Type');
153
154 // string
155 if ('string' == typeof val) {
156 if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
157 this.length = Buffer.byteLength(val);
158 return;
159 }
160
161 // buffer
162 if (Buffer.isBuffer(val)) {
163 if (setType) this.type = 'bin';
164 this.length = val.length;
165 return;
166 }
167
168 // stream
169 if ('function' == typeof val.pipe) {
170 onFinish(this.res, destroy.bind(null, val));
171 ensureErrorHandler(val, err => this.ctx.onerror(err));
172
173 // overwriting
174 if (null != original && original != val) this.remove('Content-Length');
175
176 if (setType) this.type = 'bin';
177 return;
178 }
179
180 // json
181 this.remove('Content-Length');
182 this.type = 'json';
183 },
184
185 /**
186 * Set Content-Length field to `n`.
187 *
188 * @param {Number} n
189 * @api public
190 */
191
192 set length(n) {
193 this.set('Content-Length', n);
194 },
195
196 /**
197 * Return parsed response Content-Length when present.
198 *
199 * @return {Number}
200 * @api public
201 */
202
203 get length() {
204 if (this.has('Content-Length')) {
205 return parseInt(this.get('Content-Length'), 10) || 0;
206 }
207
208 const { body } = this;
209 if (!body || body instanceof Stream) return undefined;
210 if ('string' === typeof body) return Buffer.byteLength(body);
211 if (Buffer.isBuffer(body)) return body.length;
212 return Buffer.byteLength(JSON.stringify(body));
213 },
214
215 /**
216 * Check if a header has been written to the socket.
217 *
218 * @return {Boolean}
219 * @api public
220 */
221
222 get headerSent() {
223 return this.res.headersSent;
224 },
225
226 /**
227 * Vary on `field`.
228 *
229 * @param {String} field
230 * @api public
231 */
232
233 vary(field) {
234 if (this.headerSent) return;
235
236 vary(this.res, field);
237 },
238
239 /**
240 * Perform a 302 redirect to `url`.
241 *
242 * The string "back" is special-cased
243 * to provide Referrer support, when Referrer
244 * is not present `alt` or "/" is used.
245 *
246 * Examples:
247 *
248 * this.redirect('back');
249 * this.redirect('back', '/index.html');
250 * this.redirect('/login');
251 * this.redirect('http://google.com');
252 *
253 * @param {String} url
254 * @param {String} [alt]
255 * @api public
256 */
257
258 redirect(url, alt) {
259 // location
260 if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
261 this.set('Location', encodeUrl(url));
262
263 // status
264 if (!statuses.redirect[this.status]) this.status = 302;
265
266 // html
267 if (this.ctx.accepts('html')) {
268 url = escape(url);
269 this.type = 'text/html; charset=utf-8';
270 this.body = `Redirecting to <a href="${url}">${url}</a>.`;
271 return;
272 }
273
274 // text
275 this.type = 'text/plain; charset=utf-8';
276 this.body = `Redirecting to ${url}.`;
277 },
278
279 /**
280 * Set Content-Disposition header to "attachment" with optional `filename`.
281 *
282 * @param {String} filename
283 * @api public
284 */
285
286 attachment(filename, options) {
287 if (filename) this.type = extname(filename);
288 this.set('Content-Disposition', contentDisposition(filename, options));
289 },
290
291 /**
292 * Set Content-Type response header with `type` through `mime.lookup()`
293 * when it does not contain a charset.
294 *
295 * Examples:
296 *
297 * this.type = '.html';
298 * this.type = 'html';
299 * this.type = 'json';
300 * this.type = 'application/json';
301 * this.type = 'png';
302 *
303 * @param {String} type
304 * @api public
305 */
306
307 set type(type) {
308 type = getType(type);
309 if (type) {
310 this.set('Content-Type', type);
311 } else {
312 this.remove('Content-Type');
313 }
314 },
315
316 /**
317 * Set the Last-Modified date using a string or a Date.
318 *
319 * this.response.lastModified = new Date();
320 * this.response.lastModified = '2013-09-13';
321 *
322 * @param {String|Date} type
323 * @api public
324 */
325
326 set lastModified(val) {
327 if ('string' == typeof val) val = new Date(val);
328 this.set('Last-Modified', val.toUTCString());
329 },
330
331 /**
332 * Get the Last-Modified date in Date form, if it exists.
333 *
334 * @return {Date}
335 * @api public
336 */
337
338 get lastModified() {
339 const date = this.get('last-modified');
340 if (date) return new Date(date);
341 },
342
343 /**
344 * Set the ETag of a response.
345 * This will normalize the quotes if necessary.
346 *
347 * this.response.etag = 'md5hashsum';
348 * this.response.etag = '"md5hashsum"';
349 * this.response.etag = 'W/"123456789"';
350 *
351 * @param {String} etag
352 * @api public
353 */
354
355 set etag(val) {
356 if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
357 this.set('ETag', val);
358 },
359
360 /**
361 * Get the ETag of a response.
362 *
363 * @return {String}
364 * @api public
365 */
366
367 get etag() {
368 return this.get('ETag');
369 },
370
371 /**
372 * Return the response mime type void of
373 * parameters such as "charset".
374 *
375 * @return {String}
376 * @api public
377 */
378
379 get type() {
380 const type = this.get('Content-Type');
381 if (!type) return '';
382 return type.split(';', 1)[0];
383 },
384
385 /**
386 * Check whether the response is one of the listed types.
387 * Pretty much the same as `this.request.is()`.
388 *
389 * @param {String|String[]} [type]
390 * @param {String[]} [types]
391 * @return {String|false}
392 * @api public
393 */
394
395 is(type, ...types) {
396 return typeis(this.type, type, ...types);
397 },
398
399 /**
400 * Return response header.
401 *
402 * Examples:
403 *
404 * this.get('Content-Type');
405 * // => "text/plain"
406 *
407 * this.get('content-type');
408 * // => "text/plain"
409 *
410 * @param {String} field
411 * @return {String}
412 * @api public
413 */
414
415 get(field) {
416 return this.header[field.toLowerCase()] || '';
417 },
418
419 /**
420 * Returns true if the header identified by name is currently set in the outgoing headers.
421 * The header name matching is case-insensitive.
422 *
423 * Examples:
424 *
425 * this.has('Content-Type');
426 * // => true
427 *
428 * this.get('content-type');
429 * // => true
430 *
431 * @param {String} field
432 * @return {boolean}
433 * @api public
434 */
435 has(field) {
436 return typeof this.res.hasHeader === 'function'
437 ? this.res.hasHeader(field)
438 // Node < 7.7
439 : field.toLowerCase() in this.headers;
440 },
441
442 /**
443 * Set header `field` to `val`, or pass
444 * an object of header fields.
445 *
446 * Examples:
447 *
448 * this.set('Foo', ['bar', 'baz']);
449 * this.set('Accept', 'application/json');
450 * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
451 *
452 * @param {String|Object|Array} field
453 * @param {String} val
454 * @api public
455 */
456
457 set(field, val) {
458 if (this.headerSent) return;
459
460 if (2 == arguments.length) {
461 if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
462 else if (typeof val !== 'string') val = String(val);
463 this.res.setHeader(field, val);
464 } else {
465 for (const key in field) {
466 this.set(key, field[key]);
467 }
468 }
469 },
470
471 /**
472 * Append additional header `field` with value `val`.
473 *
474 * Examples:
475 *
476 * ```
477 * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
478 * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
479 * this.append('Warning', '199 Miscellaneous warning');
480 * ```
481 *
482 * @param {String} field
483 * @param {String|Array} val
484 * @api public
485 */
486
487 append(field, val) {
488 const prev = this.get(field);
489
490 if (prev) {
491 val = Array.isArray(prev)
492 ? prev.concat(val)
493 : [prev].concat(val);
494 }
495
496 return this.set(field, val);
497 },
498
499 /**
500 * Remove header `field`.
501 *
502 * @param {String} name
503 * @api public
504 */
505
506 remove(field) {
507 if (this.headerSent) return;
508
509 this.res.removeHeader(field);
510 },
511
512 /**
513 * Checks if the request is writable.
514 * Tests for the existence of the socket
515 * as node sometimes does not set it.
516 *
517 * @return {Boolean}
518 * @api private
519 */
520
521 get writable() {
522 // can't write any more after response finished
523 // response.writableEnded is available since Node > 12.9
524 // https://nodejs.org/api/http.html#http_response_writableended
525 // response.finished is undocumented feature of previous Node versions
526 // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js
527 if (this.res.writableEnded || this.res.finished) return false;
528
529 const socket = this.res.socket;
530 // There are already pending outgoing res, but still writable
531 // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
532 if (!socket) return true;
533 return socket.writable;
534 },
535
536 /**
537 * Inspect implementation.
538 *
539 * @return {Object}
540 * @api public
541 */
542
543 inspect() {
544 if (!this.res) return;
545 const o = this.toJSON();
546 o.body = this.body;
547 return o;
548 },
549
550 /**
551 * Return JSON representation.
552 *
553 * @return {Object}
554 * @api public
555 */
556
557 toJSON() {
558 return only(this, [
559 'status',
560 'message',
561 'header'
562 ]);
563 },
564
565 /**
566 * Flush any set headers, and begin the body
567 */
568 flushHeaders() {
569 this.res.flushHeaders();
570 }
571};
572
573/**
574 * Custom inspection implementation for newer Node.js versions.
575 *
576 * @return {Object}
577 * @api public
578 */
579if (util.inspect.custom) {
580 module.exports[util.inspect.custom] = module.exports.inspect;
581}