Spaces:
Sleeping
Sleeping
| ; | |
| /** | |
| * Module dependencies. | |
| */ | |
| const contentDisposition = require('content-disposition'); | |
| const getType = require('cache-content-type'); | |
| const onFinish = require('on-finished'); | |
| const escape = require('escape-html'); | |
| const typeis = require('type-is').is; | |
| const statuses = require('statuses'); | |
| const destroy = require('destroy'); | |
| const assert = require('assert'); | |
| const extname = require('path').extname; | |
| const vary = require('vary'); | |
| const only = require('only'); | |
| const util = require('util'); | |
| const encodeUrl = require('encodeurl'); | |
| const Stream = require('stream'); | |
| /** | |
| * Prototype. | |
| */ | |
| module.exports = { | |
| /** | |
| * Return the request socket. | |
| * | |
| * @return {Connection} | |
| * @api public | |
| */ | |
| get socket() { | |
| return this.res.socket; | |
| }, | |
| /** | |
| * Return response header. | |
| * | |
| * @return {Object} | |
| * @api public | |
| */ | |
| get header() { | |
| const { res } = this; | |
| return typeof res.getHeaders === 'function' | |
| ? res.getHeaders() | |
| : res._headers || {}; // Node < 7.7 | |
| }, | |
| /** | |
| * Return response header, alias as response.header | |
| * | |
| * @return {Object} | |
| * @api public | |
| */ | |
| get headers() { | |
| return this.header; | |
| }, | |
| /** | |
| * Get response status code. | |
| * | |
| * @return {Number} | |
| * @api public | |
| */ | |
| get status() { | |
| return this.res.statusCode; | |
| }, | |
| /** | |
| * Set response status code. | |
| * | |
| * @param {Number} code | |
| * @api public | |
| */ | |
| set status(code) { | |
| if (this.headerSent) return; | |
| assert(Number.isInteger(code), 'status code must be a number'); | |
| assert(code >= 100 && code <= 999, `invalid status code: ${code}`); | |
| this._explicitStatus = true; | |
| this.res.statusCode = code; | |
| if (this.req.httpVersionMajor < 2) 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; | |
| }, | |
| /** | |
| * 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) { | |
| const original = this._body; | |
| this._body = val; | |
| // no content | |
| if (null == val) { | |
| if (!statuses.empty[this.status]) this.status = 204; | |
| if (val === null) this._explicitNullBody = true; | |
| this.remove('Content-Type'); | |
| this.remove('Content-Length'); | |
| this.remove('Transfer-Encoding'); | |
| return; | |
| } | |
| // set the status | |
| if (!this._explicitStatus) this.status = 200; | |
| // set the content-type only if not yet set | |
| const setType = !this.has('Content-Type'); | |
| // string | |
| if ('string' === typeof val) { | |
| if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'; | |
| this.length = Buffer.byteLength(val); | |
| return; | |
| } | |
| // buffer | |
| if (Buffer.isBuffer(val)) { | |
| if (setType) this.type = 'bin'; | |
| this.length = val.length; | |
| return; | |
| } | |
| // stream | |
| if (val instanceof Stream) { | |
| onFinish(this.res, destroy.bind(null, val)); | |
| if (original != val) { | |
| val.once('error', err => this.ctx.onerror(err)); | |
| // overwriting | |
| if (null != original) this.remove('Content-Length'); | |
| } | |
| if (setType) this.type = 'bin'; | |
| return; | |
| } | |
| // json | |
| this.remove('Content-Length'); | |
| this.type = 'json'; | |
| }, | |
| /** | |
| * Set Content-Length field to `n`. | |
| * | |
| * @param {Number} n | |
| * @api public | |
| */ | |
| set length(n) { | |
| if (!this.has('Transfer-Encoding')) { | |
| this.set('Content-Length', n); | |
| } | |
| }, | |
| /** | |
| * Return parsed response Content-Length when present. | |
| * | |
| * @return {Number} | |
| * @api public | |
| */ | |
| get length() { | |
| if (this.has('Content-Length')) { | |
| return parseInt(this.get('Content-Length'), 10) || 0; | |
| } | |
| const { body } = this; | |
| if (!body || body instanceof Stream) return undefined; | |
| if ('string' === typeof body) return Buffer.byteLength(body); | |
| if (Buffer.isBuffer(body)) return body.length; | |
| return Buffer.byteLength(JSON.stringify(body)); | |
| }, | |
| /** | |
| * 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(field) { | |
| if (this.headerSent) return; | |
| vary(this.res, field); | |
| }, | |
| /** | |
| * 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 | |
| * @param {String} [alt] | |
| * @api public | |
| */ | |
| redirect(url, alt) { | |
| // location | |
| if ('back' === url) url = this.ctx.get('Referrer') || alt || '/'; | |
| this.set('Location', encodeUrl(url)); | |
| // status | |
| if (!statuses.redirect[this.status]) this.status = 302; | |
| // 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 | |
| this.type = 'text/plain; charset=utf-8'; | |
| this.body = `Redirecting to ${url}.`; | |
| }, | |
| /** | |
| * Set Content-Disposition header to "attachment" with optional `filename`. | |
| * | |
| * @param {String} filename | |
| * @api public | |
| */ | |
| attachment(filename, options) { | |
| if (filename) this.type = extname(filename); | |
| this.set('Content-Disposition', contentDisposition(filename, options)); | |
| }, | |
| /** | |
| * Set Content-Type response header with `type` through `mime.lookup()` | |
| * when it does not contain a charset. | |
| * | |
| * Examples: | |
| * | |
| * this.type = '.html'; | |
| * this.type = 'html'; | |
| * this.type = 'json'; | |
| * this.type = 'application/json'; | |
| * this.type = 'png'; | |
| * | |
| * @param {String} type | |
| * @api public | |
| */ | |
| set type(type) { | |
| type = getType(type); | |
| if (type) { | |
| this.set('Content-Type', type); | |
| } else { | |
| this.remove('Content-Type'); | |
| } | |
| }, | |
| /** | |
| * 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() { | |
| const 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) { | |
| if (!/^(W\/)?"/.test(val)) val = `"${val}"`; | |
| this.set('ETag', val); | |
| }, | |
| /** | |
| * Get the ETag of a response. | |
| * | |
| * @return {String} | |
| * @api public | |
| */ | |
| get etag() { | |
| return this.get('ETag'); | |
| }, | |
| /** | |
| * Return the response mime type void of | |
| * parameters such as "charset". | |
| * | |
| * @return {String} | |
| * @api public | |
| */ | |
| get type() { | |
| const type = this.get('Content-Type'); | |
| if (!type) return ''; | |
| return type.split(';', 1)[0]; | |
| }, | |
| /** | |
| * Check whether the response is one of the listed types. | |
| * Pretty much the same as `this.request.is()`. | |
| * | |
| * @param {String|String[]} [type] | |
| * @param {String[]} [types] | |
| * @return {String|false} | |
| * @api public | |
| */ | |
| is(type, ...types) { | |
| return typeis(this.type, type, ...types); | |
| }, | |
| /** | |
| * Return response header. | |
| * | |
| * Examples: | |
| * | |
| * this.get('Content-Type'); | |
| * // => "text/plain" | |
| * | |
| * this.get('content-type'); | |
| * // => "text/plain" | |
| * | |
| * @param {String} field | |
| * @return {String} | |
| * @api public | |
| */ | |
| get(field) { | |
| return this.header[field.toLowerCase()] || ''; | |
| }, | |
| /** | |
| * Returns true if the header identified by name is currently set in the outgoing headers. | |
| * The header name matching is case-insensitive. | |
| * | |
| * Examples: | |
| * | |
| * this.has('Content-Type'); | |
| * // => true | |
| * | |
| * this.get('content-type'); | |
| * // => true | |
| * | |
| * @param {String} field | |
| * @return {boolean} | |
| * @api public | |
| */ | |
| has(field) { | |
| return typeof this.res.hasHeader === 'function' | |
| ? this.res.hasHeader(field) | |
| // Node < 7.7 | |
| : field.toLowerCase() in this.headers; | |
| }, | |
| /** | |
| * 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(field, val) { | |
| if (this.headerSent) return; | |
| if (2 === arguments.length) { | |
| if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v)); | |
| else if (typeof val !== 'string') val = String(val); | |
| this.res.setHeader(field, val); | |
| } else { | |
| for (const key in field) { | |
| this.set(key, field[key]); | |
| } | |
| } | |
| }, | |
| /** | |
| * 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(field, val) { | |
| const prev = this.get(field); | |
| if (prev) { | |
| val = Array.isArray(prev) | |
| ? prev.concat(val) | |
| : [prev].concat(val); | |
| } | |
| return this.set(field, val); | |
| }, | |
| /** | |
| * Remove header `field`. | |
| * | |
| * @param {String} name | |
| * @api public | |
| */ | |
| remove(field) { | |
| if (this.headerSent) return; | |
| this.res.removeHeader(field); | |
| }, | |
| /** | |
| * Checks if the request is writable. | |
| * Tests for the existence of the socket | |
| * as node sometimes does not set it. | |
| * | |
| * @return {Boolean} | |
| * @api private | |
| */ | |
| get writable() { | |
| // can't write any more after response finished | |
| // response.writableEnded is available since Node > 12.9 | |
| // https://nodejs.org/api/http.html#http_response_writableended | |
| // response.finished is undocumented feature of previous Node versions | |
| // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js | |
| if (this.res.writableEnded || this.res.finished) return false; | |
| const socket = this.res.socket; | |
| // 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; | |
| return socket.writable; | |
| }, | |
| /** | |
| * Inspect implementation. | |
| * | |
| * @return {Object} | |
| * @api public | |
| */ | |
| inspect() { | |
| if (!this.res) return; | |
| const o = this.toJSON(); | |
| o.body = this.body; | |
| return o; | |
| }, | |
| /** | |
| * Return JSON representation. | |
| * | |
| * @return {Object} | |
| * @api public | |
| */ | |
| toJSON() { | |
| return only(this, [ | |
| 'status', | |
| 'message', | |
| 'header' | |
| ]); | |
| }, | |
| /** | |
| * Flush any set headers and begin the body | |
| */ | |
| flushHeaders() { | |
| this.res.flushHeaders(); | |
| } | |
| }; | |
| /** | |
| * Custom inspection implementation for node 6+. | |
| * | |
| * @return {Object} | |
| * @api public | |
| */ | |
| /* istanbul ignore else */ | |
| if (util.inspect.custom) { | |
| module.exports[util.inspect.custom] = module.exports.inspect; | |
| } | |