UNPKG

@adonisjs/framework

Version:

Adonis framework makes it easy for you to write webapps with less code

648 lines (595 loc) 14.3 kB
'use strict' /* * adonis-framework * * (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 nodeRes = require('node-res') const methods = require('node-res/methods') const nodeReq = require('node-req') const nodeCookie = require('node-cookie') const Macroable = require('macroable') const parseurl = require('parseurl') const GE = require('@adonisjs/generic-exceptions') const RouteManager = require('../Route/Manager') const _ = require('lodash') /** * Abort exception is raised when `response.abortIf` or * `response.abortUnless` called. * * @class AbortException * @constructor */ class AbortException extends GE.HttpException { /** * Return error object with body and status * * @method invoke * * @param {String} [body = 'Request aborted'] * @param {Number} [status = 400] * * @return {AbortException} */ static invoke (body, status) { const error = new this('Request aborted', status || 400) error.body = body || 'Request aborted' return error } /** * Handling the exception itself. * * @method handle * * @param {Object} error * @param {Object} options.response * @param {Object} options.session * * @return {void} */ async handle (error, { response, session }) { /** * Commit session when in use */ if (session && typeof (session.commit) === 'function') { await session.commit() } response.status(error.status).send(error.body) } } const SECRET = 'app.appKey' const JSONPCALLBACK = 'app.http.jsonpCallback' /** * A facade over Node.js HTTP `res` object, making it * easier and simpler to make HTTP response. You can * access the original **response** object as * `response.response` * * @binding Adonis/Src/Response * @group Http * * @class Response */ class Response extends Macroable { constructor (adonisRequest, Config) { super() /** * Reference to adonisjs request * * @type {Request} */ this.adonisRequest = adonisRequest /** * Reference to native HTTP request object * * @attribute request * @type {Object} */ this.request = adonisRequest.request /** * Reference to native HTTP response object * * @attribute response * @type {Object} */ this.response = adonisRequest.response /** * Here we store the body of the response and wait * for the entire HTTP life-cycle to finish unless * we end the response. This gives a chance to * modify the response via middlewares executed * after the route handler or controller method. * * @attribute _lazyBody * * @type {Object} * @private */ this._lazyBody = { method: 'send', content: null, args: [] } /** * Implicitly end the response. If you set it * to false, calling `response.end` will * end the response. * * @type {Boolean} */ this.implicitEnd = true this.Config = Config } /** * lazyBody to be set as the response body. * * @method lazyBody * * @return {Object} */ get lazyBody () { return this._lazyBody } /** * returns whether request has been * finished or not * * @attribute finished * * @return {Boolean} */ get finished () { return this.response.finished } /** * returns whether request headers * have been sent or not * * @attribute headersSent * * @return {Boolean} */ get headersSent () { return this.response.headersSent } /** * returns whether a request is pending * or not * * @attribute isPending * * @return {Boolean} */ get isPending () { return (!this.headersSent && !this.finished) } /** * Returns a boolean indicating whether etag should be generated * for a request or not. * * @method _generateEtag * * @param {Boolean} setToTrue * @param {Boolean} hasBody * * @return {Boolean} * * @private */ _generateEtag (setToTrue, hasBody) { const statusCode = this.response.statusCode return setToTrue && hasBody && this.request.method === 'GET' && ((statusCode >= 200 && statusCode < 300) || statusCode === 304) } /** * Writes response to the res object * * @method _writeResponse * * @param {String} method * @param {Mixed} content * @param {Array} args * * @return {void} * * @private */ _writeResponse (method, content, args) { if (['send', 'json', 'jsonp'].indexOf(method) === -1) { return nodeRes[method](...[this.request, this.response, content].concat(args)) } /** * Finding whether or not to generate the etag for the response */ const generateEtag = method === 'jsonp' ? args[1] : args[0] /** * Chunk to send over the wire */ let chunk = method === 'jsonp' ? nodeRes.prepareJsonp(this.response, content, args[0]) : nodeRes.prepare(this.response, content) /** * For HEAD requests, response body should be null */ if (this.request.method === 'HEAD') { chunk = null } /** * When chunk exists and user wants etags, then we should generate one */ if (this._generateEtag(generateEtag, !!chunk)) { nodeRes.etag(this.response, chunk) if (this.adonisRequest.fresh()) { this.status(304) chunk = null } } nodeRes.end(this.response, chunk) } /** * Sets the response body. If implicitEnd is set to `false`, * then it will end the response right away, otherwise * will store it to be sent later. * * @method _invoke * * @param {String} method * @param {Mixed} content * @param {Array} args * * @return {void} * * @private */ _invoke (method, content, args) { if (!this.implicitEnd) { this._writeResponse(method, content, args) return } this._lazyBody = { method, content, args } } /** * Set the response status code. * * @method status * * @param {Number} statusCode * * @chainable */ status (statusCode) { nodeRes.status(this.response, statusCode) return this } /** * Set HTTP response header. Resetting same header * multiple times will append to the existing * value. * * @method header * * @param {String} key * @param {String} value * * @chainable */ header (key, value) { nodeRes.header(this.response, key, value) return this } /** * Set HTTP response header only if it does not * exists already * * @method safeHeader * * @param {String} key * @param {String} value * * @chainable */ safeHeader (key, value) { nodeRes.safeHeader(this.response, key, value) return this } /** * Remove the existing HTTP response header. * * @method removeHeader * * @param {String} key * * @chainable */ removeHeader (key) { nodeRes.removeHeader(this.response, key) return this } /** * Returns the value of header for a given key. * * @method getHeader * * @param {String} key * * @return {Mixed} */ getHeader (key) { return nodeRes.getHeader(this.response, key) } /** * Stream a file to the client as HTTP response. * * Options are passed directly to [send](https://www.npmjs.com/package/send) * * @method download * * @param {String} filePath * @param {Object} options * * @return {void} */ download (filePath, options = {}) { this.implicitEnd = false nodeRes.download(this.request, this.response, filePath, options) } /** * Force download the file by setting `Content-disposition` * header. * * @method attachment * * @param {String} filePath * @param {String} [name] * @param {String} [disposition] * @param {Object} [options = {}] * * @return {void} */ attachment (filePath, name, disposition, options = {}) { this.implicitEnd = false nodeRes.attachment(this.request, this.response, filePath, name, disposition, options) } /** * Set the `Location` header on HTTP response. * * @method location * * @param {String} url * * @chainable */ location (url) { nodeRes.location(this.response, url) return this } /** * Redirect the request by setting the `Location` * header and ending the response * * @method redirect * * @param {String} url * @param {Boolean} [sendParams = false] * @param {Number} [status = 302] * * @return {void} */ redirect (url, sendParams = false, status = 302) { if (url === 'back') { url = nodeReq.header(this.request, 'referrer') || '/' } /** * Send query params of the current URL back when * redirect to a new url */ if (sendParams) { const { query } = parseurl(this.request) url = query ? `${url}?${query}` : url } this._invoke('redirect', url, [status]) } /** * Redirect to a specific route * * @method route * * @param {String} routeNameOrHandler * @param {Object} [data = {}] * @param {String} [domain] * @param {Boolean} [sendParams = false] * @param {Number} [status = 302] * * @return {void} */ route (routeNameOrHandler, data, domain, sendParams = false, status = 302) { const url = RouteManager.url(routeNameOrHandler, data, domain) || routeNameOrHandler return this.redirect(url, sendParams, status) } /** * Add the HTTP `Vary` header * * @method vary * * @param {String} field * * @chainable */ vary (field) { nodeRes.vary(this.response, field) return this } /** * Sets the `Content-type` header based on the * type passed to this method. * * @method type * * @param {String} type * @param {String} [charset] * * @chainable */ type (type, charset) { nodeRes.type(this.response, type, charset) return this } /** * Sets the response body for the HTTP request. * * @method send * * @param {*} body * @param {Boolean} generateEtag * * @return {void} */ send (body, generateEtag = this.Config.get('app.http.etag')) { this._invoke('send', body, [generateEtag]) } /** * Sets the response body for the HTTP request with * explicit `content-type` set to `application/json`. * * @method json * * @param {Object} body * @param {Boolean} generateEtag * * @return {void} */ json (body, generateEtag = this.Config.get('app.http.etag')) { this._invoke('json', body, [generateEtag]) } /** * Sets the response body for the HTTP request with * explicit `content-type` set to `text/javascript`. * * @method jsonp * * @param {Object} body * @param {String} [callbackFn = 'callback'] - Callback name. * @param {Boolean} generateEtag * * @return {void} */ jsonp (body, callbackFn, generateEtag = this.Config.get('app.http.etag')) { callbackFn = callbackFn || nodeReq.get(this.request).callback || this.Config.get(JSONPCALLBACK) this._invoke('jsonp', body, [callbackFn, generateEtag]) } /** * Ends the response by setting the `_lazyBody` as the * response body. * * @method end * * @return {void} */ end () { if (this.implicitEnd) { this._writeResponse(this._lazyBody.method || 'send', this._lazyBody.content, this._lazyBody.args) } } /** * Send cookie with the http response * * @method cookie * * @param {String} key * @param {Mixed} value * @param {Object} [options = {}] * * @return {void} */ cookie (key, value, options = {}) { nodeCookie.create(this.response, key, value, options, this.Config.get(SECRET), true) } /** * Set plain cookie HTTP response * * @method plainCookie * * @param {String} key * @param {Mixed} value * @param {Object} [options = {}] * * @return {void} */ plainCookie (key, value, options) { nodeCookie.create(this.response, key, value, options) } /** * Remove existing cookie using it's key * * @method clearCookie * * @param {String} key * * @param {Object} [options = {}] * * @return {void} */ clearCookie (key, options = { path: '/' }) { nodeCookie.clear(this.response, key, options) } /** * Aborts the request (when expression is truthy) by throwing an exception. * Since AdonisJs allows exceptions to handle themselves, it simply makes * an response when handling itself. * * @method abortIf * * @param {Mixed} expression * @param {Number} status * @param {Mixed} body * * @return {void} * * @throws {AbortException} If expression is thruthy */ abortIf (expression, status, body) { const value = typeof (expression) === 'function' ? expression() : expression if (value) { throw AbortException.invoke(body, status) } } /** * Aborts the request (when expression is falsy) by throwing an exception. * Since AdonisJs allows exceptions to handle themselves, it simply makes * an response when handling itself. * * @method abortUnless * * @param {Mixed} expression * @param {Number} status * @param {Mixed} body * * @return {void} * * @throws {AbortException} If expression is falsy */ abortUnless (expression, status, body) { const value = typeof (expression) === 'function' ? expression() : expression if (!value) { throw AbortException.invoke(body, status) } } } /** * Defining _macros and _getters property * for Macroable class * * @type {Object} */ Response._macros = {} Response._getters = {} /** * Setting descriptive methods on the response prototype. */ _.each(methods, (statusCode, method) => { Response.prototype[method] = function (content, generateEtag = this.Config.get('app.http.etag')) { this.status(statusCode) this._invoke('send', content, [generateEtag]) } }) module.exports = Response