koa-lite/lib/response.js

559 lines
11 KiB
JavaScript
Raw Permalink Normal View History

'use strict';
/**
* Module dependencies.
*/
const contentDisposition = require('content-disposition');
const ensureErrorHandler = require('error-inject');
const getType = require('cache-content-type');
const onFinish = require('on-finished');
const isJSON = require('koa-is-json');
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');
/**
* Prototype.
*/
module.exports = {
2013-11-13 21:17:16 +00:00
/**
* Return the request socket.
*
* @return {Connection}
* @api public
*/
get socket() {
return this.res.socket;
2013-11-13 21:17:16 +00:00
},
/**
* 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.
*
2014-06-07 23:26:19 +00:00
* @param {Number} code
* @api public
*/
2014-03-13 01:29:14 +00:00
set status(code) {
if (this.headerSent) return;
2014-06-10 21:33:13 +00:00
assert('number' == typeof code, 'status code must be a number');
assert(statuses[code], `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;
this.remove('Content-Type');
this.remove('Content-Length');
this.remove('Transfer-Encoding');
return;
}
2013-12-30 06:26:19 +00:00
// set the status
if (!this._explicitStatus) this.status = 200;
2013-12-30 06:26:19 +00:00
// set the content-type only if not yet set
const setType = !this.header['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 ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, err => this.ctx.onerror(err));
// overwriting
if (null != original && original != val) 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) {
this.set('Content-Length', n);
},
/**
* Return parsed response Content-Length when present.
*
* @return {Number}
* @api public
*/
get length() {
const len = this.header['content-length'];
const body = this.body;
if (null == len) {
if (!body) return;
if ('string' == typeof body) return Buffer.byteLength(body);
if (Buffer.isBuffer(body)) return body.length;
if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
return;
}
return ~~len;
},
/**
* 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
*/
2015-10-12 06:51:31 +00:00
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
*/
2015-10-12 06:51:31 +00:00
redirect(url, alt) {
2014-04-12 17:59:04 +00:00
// location
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
this.set('Location', url);
2014-04-12 17:59:04 +00:00
// 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
*/
2014-03-24 18:21:15 +00:00
set type(type) {
2017-05-05 17:27:11 +00:00
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);
},
2014-06-05 23:19:29 +00:00
/**
* Get the ETag of a response.
*
* @return {String}
* @api public
*/
get etag() {
return this.get('ETag');
},
/**
2015-08-25 08:49:05 +00:00
* Return the response mime type void of
* parameters such as "charset".
*
* @return {String}
* @api public
*/
get type() {
const type = this.get('Content-Type');
2015-04-28 17:44:02 +00:00
if (!type) return '';
return type.split(';')[0];
},
2014-06-13 07:30:59 +00:00
/**
* Check whether the response is one of the listed types.
* Pretty much the same as `this.request.is()`.
*
* @param {String|Array} types...
* @return {String|false}
* @api public
*/
2015-10-12 06:51:31 +00:00
is(types) {
const type = this.type;
2014-06-13 07:30:59 +00:00
if (!types) return type || false;
if (!Array.isArray(types)) types = [].slice.call(arguments);
return typeis(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
*/
2015-10-12 06:51:31 +00:00
get(field) {
2015-04-28 17:44:02 +00:00
return this.header[field.toLowerCase()] || '';
},
/**
* 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
*/
2015-10-12 06:51:31 +00:00
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 {
2015-10-22 22:46:47 +00:00
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
*/
2015-10-12 06:51:31 +00:00
append(field, val) {
const prev = this.get(field);
2015-01-27 18:14:57 +00:00
if (prev) {
2015-01-27 18:14:57 +00:00
val = Array.isArray(prev)
? prev.concat(val)
: [prev].concat(val);
}
2015-01-27 18:14:57 +00:00
return this.set(field, val);
},
/**
* Remove header `field`.
*
* @param {String} name
* @api public
*/
2015-10-12 06:51:31 +00:00
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
if (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
*/
2015-10-12 06:51:31 +00:00
inspect() {
2014-03-11 18:06:57 +00:00
if (!this.res) return;
const o = this.toJSON();
2013-11-14 19:34:55 +00:00
o.body = this.body;
return o;
},
/**
* Return JSON representation.
*
* @return {Object}
* @api public
*/
2015-10-12 06:51:31 +00:00
toJSON() {
return only(this, [
'status',
'message',
'header'
]);
2016-03-04 03:57:52 +00:00
},
/**
* Flush any set headers, and begin the body
*/
flushHeaders() {
this.res.flushHeaders();
}
};
/**
* Custom inspection implementation for newer Node.js versions.
*
* @return {Object}
* @api public
*/
if (util.inspect.custom) {
module.exports[util.inspect.custom] = module.exports.inspect;
}