123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588 |
- 'use strict';
- /**
- * Module dependencies.
- */
- const contentDisposition = require('content-disposition');
- const getType = require('cache-content-type');
- const onFinish = require('on-finished');
- const escape = require('escape-html');
- const typeis = require('type-is').is;
- const statuses = require('statuses');
- const destroy = require('destroy');
- const assert = require('assert');
- const extname = require('path').extname;
- const vary = require('vary');
- const only = require('only');
- const util = require('util');
- const encodeUrl = require('encodeurl');
- const Stream = require('stream');
- /**
- * Prototype.
- */
- module.exports = {
- /**
- * Return the request socket.
- *
- * @return {Connection}
- * @api public
- */
- get socket() {
- return this.res.socket;
- },
- /**
- * Return response header.
- *
- * @return {Object}
- * @api public
- */
- get header() {
- const { res } = this;
- return typeof res.getHeaders === 'function'
- ? res.getHeaders()
- : res._headers || {}; // Node < 7.7
- },
- /**
- * Return response header, alias as response.header
- *
- * @return {Object}
- * @api public
- */
- get headers() {
- return this.header;
- },
- /**
- * Get response status code.
- *
- * @return {Number}
- * @api public
- */
- get status() {
- return this.res.statusCode;
- },
- /**
- * Set response status code.
- *
- * @param {Number} code
- * @api public
- */
- set status(code) {
- if (this.headerSent) return;
- assert(Number.isInteger(code), 'status code must be a number');
- assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
- this._explicitStatus = true;
- this.res.statusCode = code;
- if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
- if (this.body && statuses.empty[code]) this.body = null;
- },
- /**
- * Get response status message
- *
- * @return {String}
- * @api public
- */
- get message() {
- return this.res.statusMessage || statuses[this.status];
- },
- /**
- * Set response status message
- *
- * @param {String} msg
- * @api public
- */
- set message(msg) {
- this.res.statusMessage = msg;
- },
- /**
- * Get response body.
- *
- * @return {Mixed}
- * @api public
- */
- get body() {
- return this._body;
- },
- /**
- * Set response body.
- *
- * @param {String|Buffer|Object|Stream} val
- * @api public
- */
- set body(val) {
- const original = this._body;
- this._body = val;
- // no content
- if (null == val) {
- if (!statuses.empty[this.status]) this.status = 204;
- if (val === null) this._explicitNullBody = true;
- this.remove('Content-Type');
- this.remove('Content-Length');
- this.remove('Transfer-Encoding');
- return;
- }
- // set the status
- if (!this._explicitStatus) this.status = 200;
- // set the content-type only if not yet set
- const setType = !this.has('Content-Type');
- // string
- if ('string' === typeof val) {
- if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
- this.length = Buffer.byteLength(val);
- return;
- }
- // buffer
- if (Buffer.isBuffer(val)) {
- if (setType) this.type = 'bin';
- this.length = val.length;
- return;
- }
- // stream
- if (val instanceof Stream) {
- onFinish(this.res, destroy.bind(null, val));
- if (original != val) {
- val.once('error', err => this.ctx.onerror(err));
- // overwriting
- if (null != original) this.remove('Content-Length');
- }
- if (setType) this.type = 'bin';
- return;
- }
- // json
- this.remove('Content-Length');
- this.type = 'json';
- },
- /**
- * Set Content-Length field to `n`.
- *
- * @param {Number} n
- * @api public
- */
- set length(n) {
- if (!this.has('Transfer-Encoding')) {
- this.set('Content-Length', n);
- }
- },
- /**
- * Return parsed response Content-Length when present.
- *
- * @return {Number}
- * @api public
- */
- get length() {
- if (this.has('Content-Length')) {
- return parseInt(this.get('Content-Length'), 10) || 0;
- }
- const { body } = this;
- if (!body || body instanceof Stream) return undefined;
- if ('string' === typeof body) return Buffer.byteLength(body);
- if (Buffer.isBuffer(body)) return body.length;
- return Buffer.byteLength(JSON.stringify(body));
- },
- /**
- * Check if a header has been written to the socket.
- *
- * @return {Boolean}
- * @api public
- */
- get headerSent() {
- return this.res.headersSent;
- },
- /**
- * Vary on `field`.
- *
- * @param {String} field
- * @api public
- */
- vary(field) {
- if (this.headerSent) return;
- vary(this.res, field);
- },
- /**
- * Perform a 302 redirect to `url`.
- *
- * The string "back" is special-cased
- * to provide Referrer support, when Referrer
- * is not present `alt` or "/" is used.
- *
- * Examples:
- *
- * this.redirect('back');
- * this.redirect('back', '/index.html');
- * this.redirect('/login');
- * this.redirect('http://google.com');
- *
- * @param {String} url
- * @param {String} [alt]
- * @api public
- */
- redirect(url, alt) {
- // location
- if ('back' === url) url = this.ctx.get('Referrer') || alt || '/';
- this.set('Location', encodeUrl(url));
- // status
- if (!statuses.redirect[this.status]) this.status = 302;
- // html
- if (this.ctx.accepts('html')) {
- url = escape(url);
- this.type = 'text/html; charset=utf-8';
- this.body = `Redirecting to <a href="${url}">${url}</a>.`;
- return;
- }
- // text
- this.type = 'text/plain; charset=utf-8';
- this.body = `Redirecting to ${url}.`;
- },
- /**
- * Set Content-Disposition header to "attachment" with optional `filename`.
- *
- * @param {String} filename
- * @api public
- */
- attachment(filename, options) {
- if (filename) this.type = extname(filename);
- this.set('Content-Disposition', contentDisposition(filename, options));
- },
- /**
- * Set Content-Type response header with `type` through `mime.lookup()`
- * when it does not contain a charset.
- *
- * Examples:
- *
- * this.type = '.html';
- * this.type = 'html';
- * this.type = 'json';
- * this.type = 'application/json';
- * this.type = 'png';
- *
- * @param {String} type
- * @api public
- */
- set type(type) {
- type = getType(type);
- if (type) {
- this.set('Content-Type', type);
- } else {
- this.remove('Content-Type');
- }
- },
- /**
- * Set the Last-Modified date using a string or a Date.
- *
- * this.response.lastModified = new Date();
- * this.response.lastModified = '2013-09-13';
- *
- * @param {String|Date} type
- * @api public
- */
- set lastModified(val) {
- if ('string' === typeof val) val = new Date(val);
- this.set('Last-Modified', val.toUTCString());
- },
- /**
- * Get the Last-Modified date in Date form, if it exists.
- *
- * @return {Date}
- * @api public
- */
- get lastModified() {
- const date = this.get('last-modified');
- if (date) return new Date(date);
- },
- /**
- * Set the ETag of a response.
- * This will normalize the quotes if necessary.
- *
- * this.response.etag = 'md5hashsum';
- * this.response.etag = '"md5hashsum"';
- * this.response.etag = 'W/"123456789"';
- *
- * @param {String} etag
- * @api public
- */
- set etag(val) {
- if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
- this.set('ETag', val);
- },
- /**
- * Get the ETag of a response.
- *
- * @return {String}
- * @api public
- */
- get etag() {
- return this.get('ETag');
- },
- /**
- * Return the response mime type void of
- * parameters such as "charset".
- *
- * @return {String}
- * @api public
- */
- get type() {
- const type = this.get('Content-Type');
- if (!type) return '';
- return type.split(';', 1)[0];
- },
- /**
- * Check whether the response is one of the listed types.
- * Pretty much the same as `this.request.is()`.
- *
- * @param {String|String[]} [type]
- * @param {String[]} [types]
- * @return {String|false}
- * @api public
- */
- is(type, ...types) {
- return typeis(this.type, type, ...types);
- },
- /**
- * Return response header.
- *
- * Examples:
- *
- * this.get('Content-Type');
- * // => "text/plain"
- *
- * this.get('content-type');
- * // => "text/plain"
- *
- * @param {String} field
- * @return {String}
- * @api public
- */
- get(field) {
- return this.header[field.toLowerCase()] || '';
- },
- /**
- * Returns true if the header identified by name is currently set in the outgoing headers.
- * The header name matching is case-insensitive.
- *
- * Examples:
- *
- * this.has('Content-Type');
- * // => true
- *
- * this.get('content-type');
- * // => true
- *
- * @param {String} field
- * @return {boolean}
- * @api public
- */
- has(field) {
- return typeof this.res.hasHeader === 'function'
- ? this.res.hasHeader(field)
- // Node < 7.7
- : field.toLowerCase() in this.headers;
- },
- /**
- * Set header `field` to `val` or pass
- * an object of header fields.
- *
- * Examples:
- *
- * this.set('Foo', ['bar', 'baz']);
- * this.set('Accept', 'application/json');
- * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
- *
- * @param {String|Object|Array} field
- * @param {String} val
- * @api public
- */
- set(field, val) {
- if (this.headerSent) return;
- if (2 === arguments.length) {
- if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
- else if (typeof val !== 'string') val = String(val);
- this.res.setHeader(field, val);
- } else {
- for (const key in field) {
- this.set(key, field[key]);
- }
- }
- },
- /**
- * Append additional header `field` with value `val`.
- *
- * Examples:
- *
- * ```
- * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
- * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
- * this.append('Warning', '199 Miscellaneous warning');
- * ```
- *
- * @param {String} field
- * @param {String|Array} val
- * @api public
- */
- append(field, val) {
- const prev = this.get(field);
- if (prev) {
- val = Array.isArray(prev)
- ? prev.concat(val)
- : [prev].concat(val);
- }
- return this.set(field, val);
- },
- /**
- * Remove header `field`.
- *
- * @param {String} name
- * @api public
- */
- remove(field) {
- if (this.headerSent) return;
- this.res.removeHeader(field);
- },
- /**
- * Checks if the request is writable.
- * Tests for the existence of the socket
- * as node sometimes does not set it.
- *
- * @return {Boolean}
- * @api private
- */
- get writable() {
- // can't write any more after response finished
- // response.writableEnded is available since Node > 12.9
- // https://nodejs.org/api/http.html#http_response_writableended
- // response.finished is undocumented feature of previous Node versions
- // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js
- if (this.res.writableEnded || this.res.finished) return false;
- const socket = this.res.socket;
- // There are already pending outgoing res, but still writable
- // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
- if (!socket) return true;
- return socket.writable;
- },
- /**
- * Inspect implementation.
- *
- * @return {Object}
- * @api public
- */
- inspect() {
- if (!this.res) return;
- const o = this.toJSON();
- o.body = this.body;
- return o;
- },
- /**
- * Return JSON representation.
- *
- * @return {Object}
- * @api public
- */
- toJSON() {
- return only(this, [
- 'status',
- 'message',
- 'header'
- ]);
- },
- /**
- * Flush any set headers and begin the body
- */
- flushHeaders() {
- this.res.flushHeaders();
- }
- };
- /**
- * Custom inspection implementation for node 6+.
- *
- * @return {Object}
- * @api public
- */
- /* istanbul ignore else */
- if (util.inspect.custom) {
- module.exports[util.inspect.custom] = module.exports.inspect;
- }
|