'use strict'
|
|
/*
|
* node-res
|
*
|
* (c) Harminder Virk <virk@adonisjs.com>
|
*
|
* 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*</.test(body) ? 'text/html' : 'text/plain'
|
}
|
}
|
|
/**
|
* If body is a buffer, return the exact copy
|
* and type as bin.
|
*/
|
if (Buffer.isBuffer(body)) {
|
return { body, type: 'application/octet-stream' }
|
}
|
|
/**
|
* If body is a number or boolean. Convert it to
|
* a string and return the type as text.
|
*/
|
if (typeof (body) === 'number' || typeof (body) === 'boolean') {
|
return { body: String(body), type: 'text/plain' }
|
}
|
|
/**
|
* Otherwise check whether body is an object or not. If yes
|
* stringify it and otherwise return the exact copy.
|
*/
|
return typeof (body) === 'object'
|
? { body: JSON.stringify(body), type: 'application/json' }
|
: { body }
|
}
|
|
/**
|
* A simple IO module to make consistent HTTP response, without
|
* worrying about underlying details.
|
*
|
* @module Response
|
*/
|
const Response = exports = module.exports = {}
|
|
/**
|
* Copying all the descriptive methods to the response object.
|
*/
|
Response.descriptiveMethods = Object.keys(methods).map((method) => {
|
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', ['<http://localhost/>', '<http://localhost:3000/>'])
|
* ```
|
*/
|
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', ['<http://localhost/>', '<http://localhost:3000/>'])
|
* ```
|
*/
|
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, '<h2> Hello world </h2>')
|
*
|
* // 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, '<h2> Hello </h2>')
|
*
|
* 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, '<h2> Hello </h2>', '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)
|
})
|
}
|