relu-core
Version:
550 lines (448 loc) • 14.9 kB
JavaScript
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var http = require('http');
var sprintf = require('util').format;
var url = require('url');
var assert = require('assert-plus');
var mime = require('mime');
var httpDate = require('./http_date');
var utils = require('./utils');
var errors = require('./errors');
///--- Globals
var InternalServerError = errors.InternalServerError;
var Response = http.ServerResponse;
///--- API
/**
* sets the cache-control header. `type` defaults to _public_,
* and options currently only takes maxAge.
* @public
* @function cache
* @param {String} type value of the header
* @param {Object} options an options object
* @returns {String} the value set to the header
*/
Response.prototype.cache = function cache(type, options) {
if (typeof (type) !== 'string') {
options = type;
type = 'public';
}
if (options && options.maxAge !== undefined) {
assert.number(options.maxAge, 'options.maxAge');
type += ', max-age=' + options.maxAge;
}
return (this.header('Cache-Control', type));
};
/**
* turns off all cache related headers.
* @public
* @function noCache
* @returns {Object} self, the response object
*/
Response.prototype.noCache = function noCache() {
// HTTP 1.1
this.header('Cache-Control', 'no-cache, no-store, must-revalidate');
// HTTP 1.0
this.header('Pragma', 'no-cache');
// Proxies
this.header('Expires', '0');
return (this);
};
/**
* Appends the provided character set to the response's Content-Type.
* e.g., res.charSet('utf-8');
* @public
* @function charSet
* @param {String} type char-set value
* @returns {Object} self, the response object
*/
Response.prototype.charSet = function charSet(type) {
assert.string(type, 'charset');
this._charSet = type;
return (this);
};
/**
* the response formatter. formats the response in preparation to send it off.
* callback only used in async formatters. restify does not ship with any
* async formatters currently.
* @public
* @function format
* @param {Object | String} body the response body to format
* @param {Function} cb callback function
* @returns {undefined}
*/
Response.prototype.format = function format(body, cb) {
var log = this.log;
var formatter;
var type = this.contentType || this.getHeader('Content-Type');
var self = this;
if (!type) {
if (this.req.accepts(this.acceptable)) {
type = this.req.accepts(this.acceptable);
}
if (!type) {
// The importance of a status code outside of the
// 2xx range probably outweighs that of unable being to
// format the response body
if (this.statusCode >= 200 && this.statusCode < 300) {
this.statusCode = 406;
}
return cb(null);
}
} else if (type.indexOf(';') !== '-1') {
type = type.split(';')[0];
}
if (!(formatter = this.formatters[type])) {
if (type.indexOf('/') === -1) {
type = mime.lookup(type);
}
if (this.acceptable.indexOf(type) === -1) {
type = 'application/octet-stream';
}
formatter = this.formatters[type] || this.formatters['*/*'];
// this is a catastrophic case - should always fall back on octet-stream
// but if for some reason that's gone, return a 500.
if (!formatter) {
log.warn({
req: self.req
}, 'no formatter found. Returning 500.');
this.statusCode = 500;
return cb(null);
}
}
if (this._charSet) {
type = type + '; charset=' + this._charSet;
}
this.setHeader('Content-Type', type);
if (body instanceof Error && body.statusCode !== undefined) {
this.statusCode = body.statusCode;
}
return (formatter.call(this, this.req, this, body, cb));
};
/**
* retrieves a header off the response.
* @public
* @function get
* @param {Object} name the header name
* @returns {String}
*/
Response.prototype.get = function get(name) {
assert.string(name, 'name');
return (this.getHeader(name));
};
/**
* retrieves all headers off the response.
* @public
* @function getHeaders
* @returns {Object}
*/
Response.prototype.getHeaders = function getHeaders() {
return (this._headers || {});
};
Response.prototype.headers = Response.prototype.getHeaders;
/**
* sets headers on the response.
* @public
* @function header
* @param {String} name the name of the header
* @param {String} value the value of the header
* @returns {Object}
*/
Response.prototype.header = function header(name, value) {
assert.string(name, 'name');
if (value === undefined) {
return (this.getHeader(name));
}
if (value instanceof Date) {
value = httpDate(value);
} else if (arguments.length > 2) {
// Support res.header('foo', 'bar %s', 'baz');
var arg = Array.prototype.slice.call(arguments).slice(2);
value = sprintf(value, arg);
}
var current = this.getHeader(name);
// #779, don't use comma separated values for set-cookie, see
// http://tools.ietf.org/html/rfc6265#section-3
if (current && name.toLowerCase() !== 'set-cookie') {
if (Array.isArray(current)) {
current.push(value);
value = current;
} else {
value = [current, value];
}
}
this.setHeader(name, value);
return (value);
};
/**
* short hand method for:
* res.contentType = 'json';
* res.send({hello: 'world'});
* @public
* @function json
* @param {Number} code http status code
* @param {Object} object value to json.stringify
* @param {Object} headers headers to set on the response
* @returns {Object}
*/
Response.prototype.json = function json(code, object, headers) {
if (!/application\/json/.test(this.header('content-type'))) {
this.header('Content-Type', 'application/json');
}
return (this.send(code, object, headers));
};
/**
* sets the link heaader.
* @public
* @function link
* @param {String} l the link key
* @param {String} rel the link value
* @returns {String} the header value set to res
*/
Response.prototype.link = function link(l, rel) {
assert.string(l, 'link');
assert.string(rel, 'rel');
var _link = sprintf('<%s>; rel="%s"', l, rel);
return (this.header('Link', _link));
};
/**
* sends the response object. convenience method that handles:
* writeHead(), write(), end()
* @public
* @function send
* @param {Number} code http status code
* @param {Object | Buffer | Error} body the content to send
* @param {Object} headers any add'l headers to set
* @returns {Object} self, the response object
*/
Response.prototype.send = function send(code, body, headers) {
var isHead = (this.req.method === 'HEAD');
var log = this.log;
var self = this;
if (code === undefined) {
this.statusCode = 200;
} else if (code.constructor.name === 'Number') {
this.statusCode = code;
if (body instanceof Error) {
body.statusCode = this.statusCode;
}
} else {
headers = body;
body = code;
code = null;
}
headers = headers || {};
if (log.trace()) {
var _props = {
code: self.statusCode,
headers: headers
};
if (body instanceof Error) {
_props.err = body;
} else {
_props.body = body;
}
log.trace(_props, 'response::send entered');
}
this._body = body;
function _cb(err, _body) {
// the problem here is that if the formatter throws an error, we can't
// actually format the error again, since the formatter already failed.
// So all we can do is send back a 500 with no body, since we don't
// know at this point what format to send the error as. Additionally,
// the current 'after' event is emitted _before_ we send the response,
// so there's no way to re-emit the error here. TODO: clean up 'after'
// even emitter so we pick up the error here.
if (err) {
self._data = null;
self.statusCode = 500;
log.error(err, 'unable to format response');
} else {
self._data = _body;
}
Object.keys(headers).forEach(function (k) {
self.setHeader(k, headers[k]);
});
self.writeHead(self.statusCode);
if (self._data && !(isHead || code === 204 || code === 304)) {
self.write(self._data);
}
self.end();
if (log.trace()) {
log.trace({res: self}, 'response sent');
}
}
if (body !== undefined) {
this.format(body, _cb);
} else {
_cb(null, null);
}
return (this);
};
/**
* sets a header on the response.
* @public
* @function set
* @param {String} name name of the header
* @param {String} val value of the header
* @returns {Object} self, the response object
*/
Response.prototype.set = function set(name, val) {
var self = this;
if (arguments.length === 2) {
assert.string(name, 'name');
this.header(name, val);
} else {
assert.object(name, 'object');
Object.keys(name).forEach(function (k) {
self.header(k, name[k]);
});
}
return (this);
};
/**
* sets the http status code on the response.
* @public
* @function status
* @param {Number} code http status code
* @returns {Number} the status code passed in
*/
Response.prototype.status = function status(code) {
assert.number(code, 'code');
this.statusCode = code;
return (code);
};
/**
* toString() serialization.
* @public
* @function toString
* @returns {String}
*/
Response.prototype.toString = function toString() {
var headers = this.getHeaders();
var headerString = '';
var str;
Object.keys(headers).forEach(function (k) {
headerString += k + ': ' + headers[k] + '\n';
});
str = sprintf('HTTP/1.1 %s %s\n%s',
this.statusCode,
http.STATUS_CODES[this.statusCode],
headerString);
return (str);
};
if (!Response.prototype.hasOwnProperty('_writeHead')) {
Response.prototype._writeHead = Response.prototype.writeHead;
}
/**
* pass through to native response.writeHead()
* @public
* @function writeHead
* @returns {undefined}
*/
Response.prototype.writeHead = function restifyWriteHead() {
this.emit('header');
if (this.statusCode === 204 || this.statusCode === 304) {
this.removeHeader('Content-Length');
this.removeHeader('Content-MD5');
this.removeHeader('Content-Type');
this.removeHeader('Content-Encoding');
}
this._writeHead.apply(this, arguments);
};
/*
* redirect is sugar method for redirecting. takes a few different signatures:
* 1) res.redirect(301, 'www.foo.com', next);
* 2) res.redirect('www.foo.com', next);
* 3) res.redirect({...}, next);
* `next` is mandatory, to complete the response and trigger audit logger.
* @public
* @param {Number | String} arg1 the status code or url to direct to
* @param {String | Function} arg2 the url to redirect to, or `next` fn
* @param {Function} arg3 `next` fn
* @function redirect
* @return {undefined}
*/
Response.prototype.redirect = function redirect(arg1, arg2, arg3) {
var self = this;
var statusCode = 302;
var finalUri;
var next;
// 1) this is signature 1, where an explicit status code is passed in.
// MUST guard against null here, passing null is likely indicative
// of an attempt to call res.redirect(null, next);
// as a way to do a reload of the current page.
if (arg1 && !isNaN(arg1)) {
statusCode = arg1;
finalUri = arg2;
next = arg3;
}
// 2) this is signaure number 2
else if (typeof (arg1) === 'string') {
// otherwise, it's a string, and use it directly
finalUri = arg1;
next = arg2;
}
// 3) signature number 3, using an options object.
else if (typeof (arg1) === 'object') {
// set next, then go to work.
next = arg2;
var req = self.req;
var opt = arg1 || {};
var currentFullPath = req.href();
var secure = (opt.hasOwnProperty('secure')) ?
opt.secure :
req.isSecure();
// if hostname is passed in, use that as the base,
// otherwise fall back on current url.
var parsedUri = url.parse(opt.hostname || currentFullPath, true);
// create the object we'll use to format for the final uri.
// this object will eventually get passed to url.format().
// can't use parsedUri to seed it, as it confuses the url module
// with some existing parsed state. instead, we'll pick the things
// we want and use that as a starting point.
finalUri = {
port: parsedUri.port,
hostname: parsedUri.hostname,
query: parsedUri.query,
pathname: parsedUri.pathname
};
// start building url based on options.
// first, set protocol.
finalUri.protocol = (secure === true) ? 'https' : 'http';
// then set host
if (opt.hostname) {
finalUri.hostname = opt.hostname;
}
// then set current path after the host
if (opt.pathname) {
finalUri.pathname = opt.pathname;
}
// then add query params
if (opt.query) {
if (opt.overrideQuery === true) {
finalUri.query = opt.query;
} else {
finalUri.query = utils.mergeQs(opt.query, finalUri.query);
}
}
// change status code to 301 permanent if specified
if (opt.permanent) {
statusCode = 301;
}
}
// if we're missing a next we should probably throw. if user wanted
// to redirect but we were unable to do so, we should not continue
// down the handler stack.
assert.func(next, 'res.redirect() requires a next param');
// if we are missing a finalized uri
// by this point, pass an error to next.
if (!finalUri) {
return (next(new InternalServerError('could not construct url')));
}
// now we're done constructing url, send the res
self.send(statusCode, null, {
location: url.format(finalUri)
});
// tell server to stop processing the handler stack.
return (next(false));
};