2013-11-13 17:01:15 +00:00
|
|
|
|
2015-10-11 22:59:51 +00:00
|
|
|
'use strict';
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
/**
|
|
|
|
* Module dependencies.
|
|
|
|
*/
|
|
|
|
|
2014-11-16 13:52:14 +00:00
|
|
|
var contentDisposition = require('content-disposition');
|
2014-04-14 22:36:39 +00:00
|
|
|
var ensureErrorHandler = require('error-inject');
|
2014-06-04 04:44:25 +00:00
|
|
|
var getType = require('mime-types').contentType;
|
2014-10-01 12:12:20 +00:00
|
|
|
var onFinish = require('on-finished');
|
2014-04-29 03:34:26 +00:00
|
|
|
var isJSON = require('koa-is-json');
|
2014-03-13 01:30:16 +00:00
|
|
|
var escape = require('escape-html');
|
2014-06-13 07:30:59 +00:00
|
|
|
var typeis = require('type-is').is;
|
2014-10-01 12:12:20 +00:00
|
|
|
var statuses = require('statuses');
|
2014-08-15 06:34:25 +00:00
|
|
|
var destroy = require('destroy');
|
2014-06-10 21:33:13 +00:00
|
|
|
var assert = require('assert');
|
2013-11-13 17:01:15 +00:00
|
|
|
var path = require('path');
|
2014-06-05 23:04:31 +00:00
|
|
|
var vary = require('vary');
|
2013-11-13 17:01:15 +00:00
|
|
|
var extname = path.extname;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prototype.
|
|
|
|
*/
|
|
|
|
|
2013-11-14 02:34:15 +00:00
|
|
|
module.exports = {
|
2013-11-13 17:01:15 +00:00
|
|
|
|
2013-11-13 21:17:16 +00:00
|
|
|
/**
|
|
|
|
* Return the request socket.
|
|
|
|
*
|
|
|
|
* @return {Connection}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
get socket() {
|
|
|
|
return this.ctx.req.socket;
|
|
|
|
},
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
/**
|
|
|
|
* Return response header.
|
|
|
|
*
|
|
|
|
* @return {Object}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
get header() {
|
|
|
|
return this.res._headers || {};
|
|
|
|
},
|
|
|
|
|
2015-04-23 09:28:02 +00:00
|
|
|
/**
|
|
|
|
* Return response header, alias as response.header
|
|
|
|
*
|
|
|
|
* @return {Object}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
get headers() {
|
|
|
|
return this.header;
|
|
|
|
},
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
/**
|
|
|
|
* Get response status code.
|
|
|
|
*
|
|
|
|
* @return {Number}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
get status() {
|
2014-04-29 04:17:30 +00:00
|
|
|
return this.res.statusCode;
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set response status code.
|
|
|
|
*
|
2014-06-07 23:26:19 +00:00
|
|
|
* @param {Number} code
|
2013-11-13 17:01:15 +00:00
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
2014-03-13 01:29:14 +00:00
|
|
|
set status(code) {
|
2014-06-10 21:33:13 +00:00
|
|
|
assert('number' == typeof code, 'status code must be a number');
|
2014-10-01 12:12:20 +00:00
|
|
|
assert(statuses[code], 'invalid status code: ' + code);
|
2014-04-29 04:17:30 +00:00
|
|
|
this._explicitStatus = true;
|
|
|
|
this.res.statusCode = code;
|
2014-10-01 12:12:20 +00:00
|
|
|
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;
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2014-04-29 03:34:26 +00:00
|
|
|
var original = this._body;
|
2013-11-13 17:01:15 +00:00
|
|
|
this._body = val;
|
|
|
|
|
|
|
|
// no content
|
|
|
|
if (null == val) {
|
2014-10-01 12:12:20 +00:00
|
|
|
if (!statuses.empty[this.status]) this.status = 204;
|
2015-12-20 21:30:54 +00:00
|
|
|
this.remove('Content-Type');
|
|
|
|
this.remove('Content-Length');
|
|
|
|
this.remove('Transfer-Encoding');
|
2013-11-13 17:01:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-12-30 06:26:19 +00:00
|
|
|
// set the status
|
2014-04-29 04:17:30 +00:00
|
|
|
if (!this._explicitStatus) this.status = 200;
|
2013-12-30 06:26:19 +00:00
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
// set the content-type only if not yet set
|
|
|
|
var setType = !this.header['content-type'];
|
|
|
|
|
|
|
|
// string
|
|
|
|
if ('string' == typeof val) {
|
2014-02-14 02:35:25 +00:00
|
|
|
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
|
2013-11-13 17:01:15 +00:00
|
|
|
this.length = Buffer.byteLength(val);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// buffer
|
|
|
|
if (Buffer.isBuffer(val)) {
|
|
|
|
if (setType) this.type = 'bin';
|
|
|
|
this.length = val.length;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// stream
|
2013-12-20 05:14:44 +00:00
|
|
|
if ('function' == typeof val.pipe) {
|
2014-08-16 09:58:06 +00:00
|
|
|
onFinish(this.res, destroy.bind(null, val));
|
2014-04-14 22:36:39 +00:00
|
|
|
ensureErrorHandler(val, this.ctx.onerror);
|
2014-04-29 03:34:26 +00:00
|
|
|
|
|
|
|
// overwriting
|
|
|
|
if (null != original && original != val) this.remove('Content-Length');
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
if (setType) this.type = 'bin';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// json
|
2014-04-29 03:34:26 +00:00
|
|
|
this.remove('Content-Length');
|
2013-11-13 17:01:15 +00:00
|
|
|
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() {
|
|
|
|
var len = this.header['content-length'];
|
|
|
|
var body = this.body;
|
|
|
|
|
|
|
|
if (null == len) {
|
|
|
|
if (!body) return;
|
|
|
|
if ('string' == typeof body) return Buffer.byteLength(body);
|
2014-04-29 03:34:26 +00:00
|
|
|
if (Buffer.isBuffer(body)) return body.length;
|
|
|
|
if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
|
|
|
|
return;
|
2013-11-13 17:01:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
|
|
|
|
vary: function(field){
|
2014-06-05 23:04:31 +00:00
|
|
|
vary(this.res, field);
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2016-02-22 10:33:08 +00:00
|
|
|
* @param {String} [alt]
|
2013-11-13 17:01:15 +00:00
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
redirect: function(url, alt){
|
2014-04-12 17:59:04 +00:00
|
|
|
// location
|
2013-11-13 17:01:15 +00:00
|
|
|
if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
|
|
|
|
this.set('Location', url);
|
2014-04-12 17:59:04 +00:00
|
|
|
|
|
|
|
// status
|
2014-10-01 12:12:20 +00:00
|
|
|
if (!statuses.redirect[this.status]) this.status = 302;
|
2013-11-13 17:01:15 +00:00
|
|
|
|
|
|
|
// 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
|
2014-11-27 17:01:15 +00:00
|
|
|
this.type = 'text/plain; charset=utf-8';
|
2013-11-13 17:01:15 +00:00
|
|
|
this.body = 'Redirecting to ' + url + '.';
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set Content-Disposition header to "attachment" with optional `filename`.
|
|
|
|
*
|
|
|
|
* @param {String} filename
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
attachment: function(filename){
|
|
|
|
if (filename) this.type = extname(filename);
|
2014-09-15 08:39:53 +00:00
|
|
|
this.set('Content-Disposition', contentDisposition(filename));
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set Content-Type response header with `type` through `mime.lookup()`
|
2014-04-09 16:34:50 +00:00
|
|
|
* when it does not contain a charset.
|
2013-11-13 17:01:15 +00:00
|
|
|
*
|
|
|
|
* 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) {
|
2015-10-19 04:15:50 +00:00
|
|
|
type = getType(type) || false;
|
|
|
|
if (type) {
|
|
|
|
this.set('Content-Type', type);
|
|
|
|
} else {
|
|
|
|
this.remove('Content-Type');
|
|
|
|
}
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
2013-11-14 03:59:49 +00:00
|
|
|
/**
|
|
|
|
* 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() {
|
|
|
|
var 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) {
|
2013-11-14 04:48:57 +00:00
|
|
|
if (!/^(W\/)?"/.test(val)) val = '"' + val + '"';
|
2013-11-14 03:59:49 +00:00
|
|
|
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');
|
|
|
|
},
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
/**
|
2015-08-25 08:49:05 +00:00
|
|
|
* Return the response mime type void of
|
2013-11-13 17:01:15 +00:00
|
|
|
* parameters such as "charset".
|
|
|
|
*
|
|
|
|
* @return {String}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
get type() {
|
|
|
|
var type = this.get('Content-Type');
|
2015-04-28 17:44:02 +00:00
|
|
|
if (!type) return '';
|
2013-11-13 17:01:15 +00:00
|
|
|
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
|
|
|
|
*/
|
|
|
|
|
|
|
|
is: function(types){
|
|
|
|
var type = this.type;
|
|
|
|
if (!types) return type || false;
|
|
|
|
if (!Array.isArray(types)) types = [].slice.call(arguments);
|
|
|
|
return typeis(type, types);
|
|
|
|
},
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
/**
|
|
|
|
* Return response header.
|
|
|
|
*
|
|
|
|
* Examples:
|
|
|
|
*
|
|
|
|
* this.get('Content-Type');
|
|
|
|
* // => "text/plain"
|
|
|
|
*
|
|
|
|
* this.get('content-type');
|
|
|
|
* // => "text/plain"
|
|
|
|
*
|
2013-11-24 13:06:35 +00:00
|
|
|
* @param {String} field
|
2013-11-13 17:01:15 +00:00
|
|
|
* @return {String}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
2013-11-24 13:06:35 +00:00
|
|
|
get: function(field){
|
2015-04-28 17:44:02 +00:00
|
|
|
return this.header[field.toLowerCase()] || '';
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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: function(field, val){
|
|
|
|
if (2 == arguments.length) {
|
|
|
|
if (Array.isArray(val)) val = val.map(String);
|
|
|
|
else val = String(val);
|
|
|
|
this.res.setHeader(field, val);
|
|
|
|
} else {
|
|
|
|
for (var key in field) {
|
|
|
|
this.set(key, field[key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-01-25 17:53:55 +00:00
|
|
|
/**
|
|
|
|
* 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: function(field, val){
|
|
|
|
var prev = this.get(field);
|
2015-01-27 18:14:57 +00:00
|
|
|
|
2015-01-25 17:53:55 +00:00
|
|
|
if (prev) {
|
2015-01-27 18:14:57 +00:00
|
|
|
val = Array.isArray(prev)
|
|
|
|
? prev.concat(val)
|
|
|
|
: [prev].concat(val);
|
2015-01-25 17:53:55 +00:00
|
|
|
}
|
|
|
|
|
2015-01-27 18:14:57 +00:00
|
|
|
return this.set(field, val);
|
2015-01-25 17:53:55 +00:00
|
|
|
},
|
|
|
|
|
2013-11-24 13:06:35 +00:00
|
|
|
/**
|
|
|
|
* Remove header `field`.
|
|
|
|
*
|
|
|
|
* @param {String} name
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
remove: function(field){
|
|
|
|
this.res.removeHeader(field);
|
|
|
|
},
|
|
|
|
|
2013-11-13 17:01:15 +00:00
|
|
|
/**
|
2014-02-15 10:06:08 +00:00
|
|
|
* Checks if the request is writable.
|
|
|
|
*
|
|
|
|
* @return {Boolean}
|
|
|
|
* @api private
|
|
|
|
*/
|
|
|
|
|
|
|
|
get writable() {
|
2016-07-22 10:20:41 +00:00
|
|
|
// can't write any more after response finished
|
|
|
|
if (this.res.finished) return false;
|
|
|
|
|
2014-02-15 10:06:08 +00:00
|
|
|
var socket = this.res.socket;
|
2016-07-22 10:20:41 +00:00
|
|
|
// 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;
|
2014-02-15 10:06:08 +00:00
|
|
|
return socket.writable;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2013-11-13 17:01:15 +00:00
|
|
|
* Inspect implementation.
|
|
|
|
*
|
|
|
|
* @return {Object}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
inspect: function(){
|
2014-03-11 18:06:57 +00:00
|
|
|
if (!this.res) return;
|
2013-11-14 19:34:55 +00:00
|
|
|
var o = this.toJSON();
|
|
|
|
o.body = this.body;
|
|
|
|
return o;
|
2013-11-13 17:01:15 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return JSON representation.
|
|
|
|
*
|
|
|
|
* @return {Object}
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
|
|
|
|
toJSON: function(){
|
|
|
|
return {
|
|
|
|
status: this.status,
|
2014-10-01 12:12:20 +00:00
|
|
|
message: this.message,
|
2013-11-13 17:01:15 +00:00
|
|
|
header: this.header
|
2015-04-28 17:44:02 +00:00
|
|
|
};
|
2013-11-13 17:01:15 +00:00
|
|
|
}
|
|
|
|
};
|