'use strict' /* * node-res * * (c) Harminder Virk * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ const mime = require('mime-types') const etag = require('etag') const vary = require('vary') const onFinished = require('on-finished') const destroy = require('destroy') const methods = require('./methods') const returnContentAndType = function (body) { /** * Return the body and it's type when * body is a string. */ if (typeof (body) === 'string') { return { body, type: /^\s* { Response[method] = function (req, res, body) { Response.status(res, methods[method]) Response.send(req, res, body) } return method }) /** * Returns the value of an existing header on * the response object * * @method getHeader * * @param {ServerResponse} res * @param {String} key * * @return {Array|String} Return type depends upon the header existing value * * @example * ```js * nodeRes.getHeader(res, 'Content-type') * ``` */ Response.getHeader = function (res, key) { return res.getHeader(key) } /** * Sets header on the response object. This method will wipe off * existing values. To append to existing values, use `append`. * * @method header * * @param {http.ServerResponse} res * @param {String} key * @param {String|Array} value * * @return {void} * * @example * ```js * nodeRes.header(res, 'Content-type', 'application/json') * * // or set an array of headers * nodeRes.header(res, 'Link', ['', '']) * ``` */ Response.header = function (res, key, value) { const values = Array.isArray(value) ? value.map(String) : value res.setHeader(key, values) } /** * Appends value to the header existing values. * * @method append * * @param {http.ServerResponse} res * @param {String} key * @param {String|Array} value * * @return {void} * * @example * ```js * nodeRes.append(res, 'Content-type', 'application/json') * * // or append an array of headers * nodeRes.append(res, 'Link', ['', '']) * ``` */ Response.append = function (res, key, value) { const previousValue = Response.getHeader(res, key) const headers = previousValue ? (Array.isArray(previousValue) ? previousValue.concat(value) : [previousValue].concat(value)) : value Response.header(res, key, headers) } /** * Set status on the HTTP res object * * @method status * * @param {http.ServerResponse} res * @param {Number} code * * @return {void} * * @example * ```js * nodeRes.status(res, 200) * ``` */ Response.status = function (res, code) { res.statusCode = code } /** * Sets the header on response object, only if it * does not exists. * * @method safeHeader * * @param {http.ServerResponse} res * @param {String} key * @param {String|Array} value * * @return {void} * * @example * ```js * nodeRes.safeHeader(res, 'Content-type', 'application/json') * ``` */ Response.safeHeader = function (res, key, value) { if (!res.getHeader(key)) { Response.header(res, key, value) } } /** * Removes the header from response * * @method removeHeader * * @param {http.ServerResponse} res * @param {String} key * * @return {void} * * @example * ```js * nodeRes.removeHeader(res, 'Content-type') * ``` */ Response.removeHeader = function (res, key) { res.removeHeader(key) } /** * Write string or buffer to the response object. * * @method write * * @param {http.ServerResponse} res * @param {String|Buffer} body * * @return {void} * * @example * ```js * nodeRes.write(res, 'Hello world') * ``` */ Response.write = function (res, body) { res.write(body) } /** * Explictly end HTTP response * * @method end * * @param {http.ServerResponse} res * @param {String|Buffer} [payload] * * @return {void} * * @example * ```js * nodeRes.end(res, 'Hello world') * ``` */ Response.end = function (res, payload) { res.end(payload) } /** * Send body as the HTTP response and end it. Also * this method will set the appropriate `Content-type` * and `Content-length`. * * If body is set to null, this method will end the response * as 204. * * @method send * * @param {http.ServerRequest} req * @param {http.ServerResponse} res * @param {String|Buffer|Object|Stream} body * @param {Boolean} [generateEtag = true] * * @return {void} * * @example * ```js * nodeRes.send(req, res, 'Hello world') * * // or html * nodeRes.send(req, res, '

Hello world

') * * // or JSON * nodeRes.send(req, res, { greeting: 'Hello world' }) * * // or Buffer * nodeRes.send(req, res, Buffer.from('Hello world', 'utf-8')) * * // Ignore etag * nodeRes.send(req, res, 'Hello world', false) * ``` */ Response.send = function (req, res, body = null, generateEtag = true) { /** * Handle streams */ if (body && typeof (body.pipe) === 'function') { Response .stream(res, body) .catch((error) => { Response.status(res, error.code === 'ENOENT' ? 404 : 500) Response.send(req, res, error.message, generateEtag) }) return } const chunk = Response.prepare(res, body) if (chunk === null || req.method === 'HEAD') { Response.end(res) return } /** * Generate etag when instructured for */ if (generateEtag) { Response.etag(res, chunk) } Response.end(res, chunk) } /** * Sets the Etag header for a given body chunk * * @method etag * * @param {http.ServerResponse} res * @param {String|Buffer} body * * @return {void} * * @example * ```js * nodeRes.etag(res, 'Hello world') * ``` */ Response.etag = function (res, body) { Response.header(res, 'ETag', etag(body)) } /** * Prepares the response body by encoding it properly. Also * sets appropriate headers based upon the body content type. * * This method is used internally by `send`, so you should * never use it when calling `send`. * * It is helpful when you want to get the final payload and end the * response at a later stage. * * @method prepare * * @param {http.ServerResponse} res * @param {Mixed} body * * @return {String} * * @example * ```js * const chunk = nodeRes.prepare(res, '

Hello

') * * if (chunk) { * nodeRes.etag(res, chunk) * * if (nodeReq.fresh(req, res)) { * chunk = null * nodeRes.status(304) * } * * nodeRes.end(chunk) * } * ``` */ Response.prepare = function (res, body) { if (body === null) { Response.status(res, 204) Response.removeHeader(res, 'Content-Type') Response.removeHeader(res, 'Content-Length') Response.removeHeader(res, 'Transfer-Encoding') return null } let { body: chunk, type } = returnContentAndType(body) /** * Remove unwanted headers when statuscode is 204 or 304 */ if (res.statusCode === 204 || res.statusCode === 304) { Response.removeHeader(res, 'Content-Type') Response.removeHeader(res, 'Content-Length') Response.removeHeader(res, 'Transfer-Encoding') return chunk } const headers = typeof res.getHeaders === 'function' ? res.getHeaders() : (res._headers || {}) /** * Setting content type. Ideally we can use `Response.type`, which * sets the right charset too. But we will be doing extra * processing for no reasons. */ if (type && !headers['content-type']) { Response.header(res, 'Content-Type', `${type}; charset=utf-8`) } /** * setting up content length as response header */ if (chunk && !headers['content-length']) { Response.header(res, 'Content-Length', Buffer.byteLength(chunk)) } return chunk } /** * Prepares response for JSONP * * @method prepareJsonp * * @param {http.ServerResponse} res * @param {Object} body * @param {String} callbackFn * * @return {String} * * @example * ```js * const chunk = nodeRes.prepareJsonp(res, '

Hello

', 'callback') * * if (chunk) { * nodeRes.etag(res, chunk) * * if (nodeReq.fresh(req, res)) { * chunk = null * nodeRes.status(304) * } * * nodeRes.end(chunk) * } * ``` */ Response.prepareJsonp = function (res, body, callbackFn) { Response.header(res, 'X-Content-Type-Options', 'nosniff') Response.safeHeader(res, 'Content-Type', 'text/javascript; charset=utf-8') const parsedBody = JSON .stringify(body) .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') /** * setting up callbackFn on response body , typeof will make * sure not to throw error of client if callbackFn is not * a function */ return '/**/ typeof ' + callbackFn + " === 'function' && " + callbackFn + '(' + parsedBody + ');' } /** * Returns the HTTP response with `Content-type` * set to `application/json`. * * @method json * * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {Object} body * @param {Boolean} [generateEtag = true] * * @return {void} * * @example * ```js * nodeRes.json(req, res, { name: 'virk' }) * nodeRes.json(req, res, [ 'virk', 'joe' ]) * ``` */ Response.json = function (req, res, body, generateEtag) { Response.safeHeader(res, 'Content-Type', 'application/json; charset=utf-8') Response.send(req, res, body, generateEtag) } /** * Make JSONP response with `Content-type` set to * `text/javascript`. * * @method jsonp * * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {Object} body * @param {String} [callbackFn = 'callback'] * @param {Boolean} [generateEtag = true] * * @return {void} * * @example * ```js * nodeRes.jsonp(req, res, { name: 'virk' }, 'callback') * ``` */ Response.jsonp = function (req, res, body, callbackFn = 'callback', generateEtag) { Response.send(req, res, Response.prepareJsonp(res, body, callbackFn), generateEtag) } /** * Set `Location` header on the HTTP response. * * @method location * * @param {http.ServerResponse} res * @param {String} url * * @return {void} */ Response.location = function (res, url) { Response.header(res, 'Location', url) } /** * Redirect the HTTP request to the given url. * * @method redirect * * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {String} url * @param {Number} [status = 302] * * @return {void} * * @example * ```js * nodeRes.redirect(req, res, '/') * ``` */ Response.redirect = function (req, res, url, status = 302) { const body = '' Response.status(res, status) Response.location(res, url) Response.send(req, res, body) } /** * Add vary header to the HTTP response. * * @method vary * * @param {http.ServerResponse} res * @param {String} field * * @return {void} */ Response.vary = function (res, field) { vary(res, field) } /** * Set content type header by looking up the actual * type and setting charset to utf8. * * ### Note * When defining custom charset, you must set pass the complete * content type, otherwise `false` will be set as the * content-type header. * * @method type * * @param {http.IncomingMessage} req * @param {http.ServerResponse} res * @param {String} [charset] * @return {void} * * @example * ```js * nodeRes.type(res, 'html') * * nodeRes.type(res, 'json') * * nodeRes.type(res, 'text/html', 'ascii') * ``` */ Response.type = function (res, type, charset) { type = charset ? `${type}; charset=${charset}` : type Response.safeHeader(res, 'Content-Type', mime.contentType(type)) } /** * Pipe stream to the response. Also this method will make sure * to destroy the stream, if request gets cancelled. * * The promise resolve when response finishes and rejects, when * stream raises errors. * * @method stream * * @param {Object} res * @param {Stream} body * * @returns {Promise} * * @example * ```js * Response.stream(res, fs.createReadStream('foo.txt')) * * // handle stream errors * Response * .stream(res, fs.createReadStream('foo.txt')) * .catch((error) => { * }) * ``` */ Response.stream = function (res, body) { return new Promise((resolve, reject) => { if (typeof (body.pipe) !== 'function') { reject(new Error('Body is not a valid stream')) return } let finished = false /** * Error in stream */ body.on('error', (error) => { if (finished) { return } finished = true destroy(body) reject(error) }) /** * Consumed stream */ body.on('end', resolve) /** * Written response */ onFinished(res, function () { finished = true destroy(body) }) /** * Pipe to res */ body.pipe(res) }) }