christmas/patched-express-response.js

1147 lines
26 KiB
JavaScript
Raw Normal View History

2020-10-29 11:14:38 -07:00
/*!
* express
* Copyright(c) 2009-2013 TJ Holowaychuk
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
2020-11-08 13:54:08 -08:00
'use strict'
2020-10-29 11:14:38 -07:00
/**
* Module dependencies.
* @private
*/
2020-11-08 13:54:08 -08:00
const Buffer = require('safe-buffer').Buffer
const contentDisposition = require('content-disposition')
const deprecate = require('depd')('express')
const encodeUrl = require('encodeurl')
const escapeHtml = require('escape-html')
const http = require('http')
const isAbsolute = require('./utils').isAbsolute
const onFinished = require('on-finished')
const path = require('path')
const statuses = require('statuses')
const merge = require('utils-merge')
const sign = require('cookie-signature').sign
const normalizeType = require('./utils').normalizeType
const normalizeTypes = require('./utils').normalizeTypes
const setCharset = require('./utils').setCharset
const cookie = require('cookie')
const send = require('send')
const extname = path.extname
const mime = send.mime
const resolve = path.resolve
const vary = require('vary')
2020-10-29 11:14:38 -07:00
/**
* Response prototype.
* @public
*/
2020-11-08 13:54:08 -08:00
const res = Object.create(http.ServerResponse.prototype)
2020-10-29 11:14:38 -07:00
/**
* Module exports.
* @public
*/
module.exports = res
/**
* Module variables.
* @private
*/
2020-11-08 13:54:08 -08:00
const charsetRegExp = /;\s*charset\s*=/
2020-10-29 11:14:38 -07:00
/**
* Set status `code`.
*
* @param {Number} code
* @return {ServerResponse}
* @public
*/
2020-11-08 13:54:08 -08:00
res.status = function status (code) {
this.statusCode = code
return this
}
2020-10-29 11:14:38 -07:00
/**
* Set Link header field with the given `links`.
*
* Examples:
*
* res.links({
* next: 'http://api.example.com/users?page=2',
* last: 'http://api.example.com/users?page=5'
* });
*
* @param {Object} links
* @return {ServerResponse}
* @public
*/
2020-11-08 13:54:08 -08:00
res.links = function (links) {
let link = this.get('Link') || ''
if (link) link += ', '
return this.set('Link', link + Object.keys(links).map(function (rel) {
return '<' + links[rel] + '>; rel="' + rel + '"'
}).join(', '))
}
2020-10-29 11:14:38 -07:00
/**
* Send a response.
*
* Examples:
*
* res.send(Buffer.from('wahoo'));
* res.send({ some: 'json' });
* res.send('<p>some html</p>');
*
* @param {string|number|boolean|object|Buffer} body
* @public
*/
2020-11-08 13:54:08 -08:00
res.send = function send (body) {
let chunk = body
let encoding
const req = this.req
let type
2020-10-29 11:14:38 -07:00
// settings
2020-11-08 13:54:08 -08:00
const app = this.app
2020-10-29 11:14:38 -07:00
// allow status / body
if (arguments.length === 2) {
// res.send(body, status) backwards compat
if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') {
2020-11-08 13:54:08 -08:00
deprecate('res.send(body, status): Use res.status(status).send(body) instead')
this.statusCode = arguments[1]
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
deprecate('res.send(status, body): Use res.status(status).send(body) instead')
this.statusCode = arguments[0]
chunk = arguments[1]
2020-10-29 11:14:38 -07:00
}
}
// disambiguate res.send(status) and res.send(status, num)
if (typeof chunk === 'number' && arguments.length === 1) {
// res.send(status) will set status message as text string
if (!this.get('Content-Type')) {
2020-11-08 13:54:08 -08:00
this.type('txt')
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
deprecate('res.send(status): Use res.sendStatus(status) instead')
this.statusCode = chunk
2020-10-29 11:14:38 -07:00
chunk = statuses[chunk]
}
switch (typeof chunk) {
// string defaulting to html
case 'string':
if (!this.get('Content-Type')) {
2020-11-08 13:54:08 -08:00
this.type('html')
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
break
2020-10-29 11:14:38 -07:00
case 'boolean':
case 'number':
case 'object':
if (chunk === null) {
2020-11-08 13:54:08 -08:00
chunk = ''
2020-10-29 11:14:38 -07:00
} else if (Buffer.isBuffer(chunk)) {
if (!this.get('Content-Type')) {
2020-11-08 13:54:08 -08:00
this.type('bin')
2020-10-29 11:14:38 -07:00
}
} else {
2020-11-08 13:54:08 -08:00
return this.json(chunk)
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
break
2020-10-29 11:14:38 -07:00
}
// write strings in utf-8
if (typeof chunk === 'string') {
2020-11-08 13:54:08 -08:00
encoding = 'utf8'
type = this.get('Content-Type')
2020-10-29 11:14:38 -07:00
// reflect this in content-type
if (typeof type === 'string') {
2020-11-08 13:54:08 -08:00
this.set('Content-Type', setCharset(type, 'utf-8'))
2020-10-29 11:14:38 -07:00
}
}
// determine if ETag should be generated
2020-11-08 13:54:08 -08:00
const etagFn = app.get('etag fn')
const generateETag = !this.get('ETag') && typeof etagFn === 'function'
2020-10-29 11:14:38 -07:00
// populate Content-Length
2020-11-08 13:54:08 -08:00
let len
2020-10-29 11:14:38 -07:00
if (chunk !== undefined) {
if (Buffer.isBuffer(chunk)) {
// get length of Buffer
len = chunk.length
} else if (!generateETag && chunk.length < 1000) {
// just calculate length when no ETag + small chunk
len = Buffer.byteLength(chunk, encoding)
} else {
// convert chunk to Buffer and calculate
chunk = Buffer.from(chunk, encoding)
2020-11-08 13:54:08 -08:00
encoding = undefined
2020-10-29 11:14:38 -07:00
len = chunk.length
}
2020-11-08 13:54:08 -08:00
this.set('Content-Length', len)
2020-10-29 11:14:38 -07:00
}
// populate ETag
2020-11-08 13:54:08 -08:00
let etag
2020-10-29 11:14:38 -07:00
if (generateETag && len !== undefined) {
if ((etag = etagFn(chunk, encoding))) {
2020-11-08 13:54:08 -08:00
this.set('ETag', etag)
2020-10-29 11:14:38 -07:00
}
}
// freshness
2020-11-08 13:54:08 -08:00
if (req.fresh) this.statusCode = 304
2020-10-29 11:14:38 -07:00
// strip irrelevant headers
2020-11-08 13:54:08 -08:00
if (this.statusCode === 204 || this.statusCode === 304) {
this.removeHeader('Content-Type')
this.removeHeader('Content-Length')
this.removeHeader('Transfer-Encoding')
chunk = ''
2020-10-29 11:14:38 -07:00
}
if (req.method === 'HEAD') {
// skip body for HEAD
2020-11-08 13:54:08 -08:00
this.end()
2020-10-29 11:14:38 -07:00
} else {
// respond
2020-11-08 13:54:08 -08:00
this.end(chunk, encoding)
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
return this
}
2020-10-29 11:14:38 -07:00
/**
* Send JSON response.
*
* Examples:
*
* res.json(null);
* res.json({ user: 'tj' });
*
* @param {string|number|boolean|object} obj
* @public
*/
2020-11-08 13:54:08 -08:00
res.json = function json (obj) {
let val = obj
2020-10-29 11:14:38 -07:00
// allow status / body
if (arguments.length === 2) {
// res.json(body, status) backwards compat
if (typeof arguments[1] === 'number') {
2020-11-08 13:54:08 -08:00
deprecate('res.json(obj, status): Use res.status(status).json(obj) instead')
this.statusCode = arguments[1]
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
deprecate('res.json(status, obj): Use res.status(status).json(obj) instead')
this.statusCode = arguments[0]
val = arguments[1]
2020-10-29 11:14:38 -07:00
}
}
// settings
2020-11-08 13:54:08 -08:00
const app = this.app
const escape = app.get('json escape')
const replacer = app.get('json replacer')
const spaces = app.get('json spaces')
const body = stringify(val, replacer, spaces, escape)
2020-10-29 11:14:38 -07:00
// content-type
if (!this.get('Content-Type')) {
2020-11-08 13:54:08 -08:00
this.set('Content-Type', 'application/json')
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
return this.send(body)
}
2020-10-29 11:14:38 -07:00
/**
* Send JSON response with JSONP callback support.
*
* Examples:
*
* res.jsonp(null);
* res.jsonp({ user: 'tj' });
*
* @param {string|number|boolean|object} obj
* @public
*/
2020-11-08 13:54:08 -08:00
res.jsonp = function jsonp (obj) {
let val = obj
2020-10-29 11:14:38 -07:00
// allow status / body
if (arguments.length === 2) {
// res.json(body, status) backwards compat
if (typeof arguments[1] === 'number') {
2020-11-08 13:54:08 -08:00
deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead')
this.statusCode = arguments[1]
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead')
this.statusCode = arguments[0]
val = arguments[1]
2020-10-29 11:14:38 -07:00
}
}
// settings
2020-11-08 13:54:08 -08:00
const app = this.app
const escape = app.get('json escape')
const replacer = app.get('json replacer')
const spaces = app.get('json spaces')
let body = stringify(val, replacer, spaces, escape)
let callback = this.req.query[app.get('jsonp callback name')]
2020-10-29 11:14:38 -07:00
// content-type
if (!this.get('Content-Type')) {
2020-11-08 13:54:08 -08:00
this.set('X-Content-Type-Options', 'nosniff')
this.set('Content-Type', 'application/json')
2020-10-29 11:14:38 -07:00
}
// fixup callback
if (Array.isArray(callback)) {
2020-11-08 13:54:08 -08:00
callback = callback[0]
2020-10-29 11:14:38 -07:00
}
// jsonp
if (typeof callback === 'string' && callback.length !== 0) {
2020-11-08 13:54:08 -08:00
this.set('X-Content-Type-Options', 'nosniff')
this.set('Content-Type', 'text/javascript')
2020-10-29 11:14:38 -07:00
// restrict callback charset
2020-11-08 13:54:08 -08:00
callback = callback.replace(/[^\[\]\w$.]/g, '')
2020-10-29 11:14:38 -07:00
// replace chars not allowed in JavaScript that are in JSON
body = body
.replace(/\u2028/g, '\\u2028')
2020-11-08 13:54:08 -08:00
.replace(/\u2029/g, '\\u2029')
2020-10-29 11:14:38 -07:00
// the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
// the typeof check is just to reduce client error noise
2020-11-08 13:54:08 -08:00
body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
return this.send(body)
}
2020-10-29 11:14:38 -07:00
/**
* Send given HTTP status code.
*
* Sets the response status to `statusCode` and the body of the
* response to the standard description from node's http.STATUS_CODES
* or the statusCode number if no description.
*
* Examples:
*
* res.sendStatus(200);
*
* @param {number} statusCode
* @public
*/
2020-11-08 13:54:08 -08:00
res.sendStatus = function sendStatus (statusCode) {
const body = statuses[statusCode] || String(statusCode)
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
this.statusCode = statusCode
this.type('txt')
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
return this.send(body)
}
2020-10-29 11:14:38 -07:00
/**
* Transfer the file at the given `path`.
*
* Automatically sets the _Content-Type_ response header field.
* The callback `callback(err)` is invoked when the transfer is complete
* or when an error occurs. Be sure to check `res.sentHeader`
* if you wish to attempt responding, as the header and some data
* may have already been transferred.
*
* Options:
*
* - `maxAge` defaulting to 0 (can be string converted by `ms`)
* - `root` root directory for relative filenames
* - `headers` object of headers to serve with file
* - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
*
* Other options are passed along to `send`.
*
* Examples:
*
* The following example illustrates how `res.sendFile()` may
* be used as an alternative for the `static()` middleware for
* dynamic situations. The code backing `res.sendFile()` is actually
* the same code, so HTTP cache support etc is identical.
*
* app.get('/user/:uid/photos/:file', function(req, res){
* var uid = req.params.uid
* , file = req.params.file;
*
* req.user.mayViewFilesFrom(uid, function(yes){
* if (yes) {
* res.sendFile('/uploads/' + uid + '/' + file);
* } else {
* res.send(403, 'Sorry! you cant see that.');
* }
* });
* });
*
* @public
*/
2020-11-08 13:54:08 -08:00
res.sendFile = function sendFile (path, options, callback) {
let done = callback
const req = this.req
const res = this
const next = req.next
let opts = options || {}
2020-10-29 11:14:38 -07:00
if (!path) {
2020-11-08 13:54:08 -08:00
throw new TypeError('path argument is required to res.sendFile')
2020-10-29 11:14:38 -07:00
}
if (typeof path !== 'string') {
throw new TypeError('path must be a string to res.sendFile')
}
// support function as second arg
if (typeof options === 'function') {
2020-11-08 13:54:08 -08:00
done = options
opts = {}
2020-10-29 11:14:38 -07:00
}
if (!opts.root && !isAbsolute(path)) {
2020-11-08 13:54:08 -08:00
throw new TypeError('path must be absolute or specify root to res.sendFile')
2020-10-29 11:14:38 -07:00
}
// create file stream
2020-11-08 13:54:08 -08:00
const pathname = encodeURI(path)
const file = send(req, pathname, opts)
2020-10-29 11:14:38 -07:00
// transfer
sendfile(res, file, opts, function (err) {
2020-11-08 13:54:08 -08:00
if (done) return done(err)
if (err && err.code === 'EISDIR') return next()
2020-10-29 11:14:38 -07:00
// next() all but write errors
if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
2020-11-08 13:54:08 -08:00
next(err)
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
})
}
2020-10-29 11:14:38 -07:00
/**
* Transfer the file at the given `path`.
*
* Automatically sets the _Content-Type_ response header field.
* The callback `callback(err)` is invoked when the transfer is complete
* or when an error occurs. Be sure to check `res.sentHeader`
* if you wish to attempt responding, as the header and some data
* may have already been transferred.
*
* Options:
*
* - `maxAge` defaulting to 0 (can be string converted by `ms`)
* - `root` root directory for relative filenames
* - `headers` object of headers to serve with file
* - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
*
* Other options are passed along to `send`.
*
* Examples:
*
* The following example illustrates how `res.sendfile()` may
* be used as an alternative for the `static()` middleware for
* dynamic situations. The code backing `res.sendfile()` is actually
* the same code, so HTTP cache support etc is identical.
*
* app.get('/user/:uid/photos/:file', function(req, res){
* var uid = req.params.uid
* , file = req.params.file;
*
* req.user.mayViewFilesFrom(uid, function(yes){
* if (yes) {
* res.sendfile('/uploads/' + uid + '/' + file);
* } else {
* res.send(403, 'Sorry! you cant see that.');
* }
* });
* });
*
* @public
*/
res.sendfile = function (path, options, callback) {
2020-11-08 13:54:08 -08:00
let done = callback
const req = this.req
const res = this
const next = req.next
let opts = options || {}
2020-10-29 11:14:38 -07:00
// support function as second arg
if (typeof options === 'function') {
2020-11-08 13:54:08 -08:00
done = options
opts = {}
2020-10-29 11:14:38 -07:00
}
// create file stream
2020-11-08 13:54:08 -08:00
const file = send(req, path, opts)
2020-10-29 11:14:38 -07:00
// transfer
sendfile(res, file, opts, function (err) {
2020-11-08 13:54:08 -08:00
if (done) return done(err)
if (err && err.code === 'EISDIR') return next()
2020-10-29 11:14:38 -07:00
// next() all but write errors
if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
2020-11-08 13:54:08 -08:00
next(err)
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
})
}
2020-10-29 11:14:38 -07:00
res.sendfile = deprecate.function(res.sendfile,
2020-11-08 13:54:08 -08:00
'res.sendfile: Use res.sendFile instead')
2020-10-29 11:14:38 -07:00
/**
* Transfer the file at the given `path` as an attachment.
*
* Optionally providing an alternate attachment `filename`,
* and optional callback `callback(err)`. The callback is invoked
* when the data transfer is complete, or when an error has
* ocurred. Be sure to check `res.headersSent` if you plan to respond.
*
* Optionally providing an `options` object to use with `res.sendFile()`.
* This function will set the `Content-Disposition` header, overriding
* any `Content-Disposition` header passed as header options in order
* to set the attachment and filename.
*
* This method uses `res.sendFile()`.
*
* @public
*/
res.download = function download (path, filename, options, callback) {
2020-11-08 13:54:08 -08:00
let done = callback
let name = filename
let opts = options || null
2020-10-29 11:14:38 -07:00
// support function as second or third arg
if (typeof filename === 'function') {
2020-11-08 13:54:08 -08:00
done = filename
name = null
2020-10-29 11:14:38 -07:00
opts = null
} else if (typeof options === 'function') {
done = options
opts = null
}
// set Content-Disposition when file is sent
2020-11-08 13:54:08 -08:00
const headers = {
2020-10-29 11:14:38 -07:00
'Content-Disposition': contentDisposition(name || path)
2020-11-08 13:54:08 -08:00
}
2020-10-29 11:14:38 -07:00
// merge user-provided headers
if (opts && opts.headers) {
2020-11-08 13:54:08 -08:00
const keys = Object.keys(opts.headers)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
2020-10-29 11:14:38 -07:00
if (key.toLowerCase() !== 'content-disposition') {
headers[key] = opts.headers[key]
}
}
}
// merge user-provided options
opts = Object.create(opts)
opts.headers = headers
// Resolve the full path for sendFile
2020-11-08 13:54:08 -08:00
const fullPath = resolve(path)
2020-10-29 11:14:38 -07:00
// send file
return this.sendFile(fullPath, opts, done)
2020-11-08 13:54:08 -08:00
}
2020-10-29 11:14:38 -07:00
/**
* Set _Content-Type_ response header with `type` through `mime.lookup()`
* when it does not contain "/", or set the Content-Type to `type` otherwise.
*
* Examples:
*
* res.type('.html');
* res.type('html');
* res.type('json');
* res.type('application/json');
* res.type('png');
*
* @param {String} type
* @return {ServerResponse} for chaining
* @public
*/
res.contentType =
2020-11-08 13:54:08 -08:00
res.type = function contentType (type) {
const ct = type.indexOf('/') === -1
2020-10-29 11:14:38 -07:00
? mime.lookup(type)
2020-11-08 13:54:08 -08:00
: type
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
return this.set('Content-Type', ct)
}
2020-10-29 11:14:38 -07:00
/**
* Respond to the Acceptable formats using an `obj`
* of mime-type callbacks.
*
* This method uses `req.accepted`, an array of
* acceptable types ordered by their quality values.
* When "Accept" is not present the _first_ callback
* is invoked, otherwise the first match is used. When
* no match is performed the server responds with
* 406 "Not Acceptable".
*
* Content-Type is set for you, however if you choose
* you may alter this within the callback using `res.type()`
* or `res.set('Content-Type', ...)`.
*
* res.format({
* 'text/plain': function(){
* res.send('hey');
* },
*
* 'text/html': function(){
* res.send('<p>hey</p>');
* },
*
* 'appliation/json': function(){
* res.send({ message: 'hey' });
* }
* });
*
* In addition to canonicalized MIME types you may
* also use extnames mapped to these types:
*
* res.format({
* text: function(){
* res.send('hey');
* },
*
* html: function(){
* res.send('<p>hey</p>');
* },
*
* json: function(){
* res.send({ message: 'hey' });
* }
* });
*
* By default Express passes an `Error`
* with a `.status` of 406 to `next(err)`
* if a match is not made. If you provide
* a `.default` callback it will be invoked
* instead.
*
* @param {Object} obj
* @return {ServerResponse} for chaining
* @public
*/
2020-11-08 13:54:08 -08:00
res.format = function (obj) {
const req = this.req
const next = req.next
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
const fn = obj.default
if (fn) delete obj.default
const keys = Object.keys(obj)
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
const key = keys.length > 0
2020-10-29 11:14:38 -07:00
? req.accepts(keys)
2020-11-08 13:54:08 -08:00
: false
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
this.vary('Accept')
2020-10-29 11:14:38 -07:00
if (key) {
2020-11-08 13:54:08 -08:00
this.set('Content-Type', normalizeType(key).value)
obj[key](req, this, next)
2020-10-29 11:14:38 -07:00
} else if (fn) {
2020-11-08 13:54:08 -08:00
fn()
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
const err = new Error('Not Acceptable')
err.status = err.statusCode = 406
err.types = normalizeTypes(keys).map(function (o) { return o.value })
next(err)
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
return this
}
2020-10-29 11:14:38 -07:00
/**
* Set _Content-Disposition_ header to _attachment_ with optional `filename`.
*
* @param {String} filename
* @return {ServerResponse}
* @public
*/
2020-11-08 13:54:08 -08:00
res.attachment = function attachment (filename) {
2020-10-29 11:14:38 -07:00
if (filename) {
2020-11-08 13:54:08 -08:00
this.type(extname(filename))
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
this.set('Content-Disposition', contentDisposition(filename))
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
return this
}
2020-10-29 11:14:38 -07:00
/**
* Append additional header `field` with value `val`.
*
* Example:
*
* res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
* res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
* res.append('Warning', '199 Miscellaneous warning');
*
* @param {String} field
* @param {String|Array} val
* @return {ServerResponse} for chaining
* @public
*/
2020-11-08 13:54:08 -08:00
res.append = function append (field, val) {
const prev = this.get(field)
let value = val
2020-10-29 11:14:38 -07:00
if (prev) {
// concat the new and prev vals
value = Array.isArray(prev) ? prev.concat(val)
: Array.isArray(val) ? [prev].concat(val)
2020-11-08 13:54:08 -08:00
: [prev, val]
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
return this.set(field, value)
}
2020-10-29 11:14:38 -07:00
/**
* Set header `field` to `val`, or pass
* an object of header fields.
*
* Examples:
*
* res.set('Foo', ['bar', 'baz']);
* res.set('Accept', 'application/json');
* res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
*
* Aliased as `res.header()`.
*
* @param {String|Object} field
* @param {String|Array} val
* @return {ServerResponse} for chaining
* @public
*/
res.set =
2020-11-08 13:54:08 -08:00
res.header = function header (field, val) {
2020-10-29 11:14:38 -07:00
if (arguments.length === 2) {
2020-11-08 13:54:08 -08:00
let value = Array.isArray(val)
2020-10-29 11:14:38 -07:00
? val.map(String)
2020-11-08 13:54:08 -08:00
: String(val)
2020-10-29 11:14:38 -07:00
// add charset to content-type
if (field.toLowerCase() === 'content-type') {
if (Array.isArray(value)) {
2020-11-08 13:54:08 -08:00
throw new TypeError('Content-Type cannot be set to an Array')
2020-10-29 11:14:38 -07:00
}
if (!charsetRegExp.test(value)) {
2020-11-08 13:54:08 -08:00
const charset = mime.charsets.lookup(value.split(';')[0])
if (charset) value += '; charset=' + charset.toLowerCase()
2020-10-29 11:14:38 -07:00
}
}
2020-11-08 13:54:08 -08:00
this.setHeader(field, value)
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
for (const key in field) {
this.set(key, field[key])
2020-10-29 11:14:38 -07:00
}
}
2020-11-08 13:54:08 -08:00
return this
}
2020-10-29 11:14:38 -07:00
/**
* Get value for header `field`.
*
* @param {String} field
* @return {String}
* @public
*/
2020-11-08 13:54:08 -08:00
res.get = function (field) {
return this.getHeader(field)
}
2020-10-29 11:14:38 -07:00
/**
* Clear cookie `name`.
*
* @param {String} name
* @param {Object} [options]
* @return {ServerResponse} for chaining
* @public
*/
2020-11-08 13:54:08 -08:00
res.clearCookie = function clearCookie (name, options) {
const opts = merge({ expires: new Date(1), path: '/' }, options)
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
return this.cookie(name, '', opts)
}
2020-10-29 11:14:38 -07:00
/**
* Set cookie `name` to `value`, with the given `options`.
*
* Options:
*
* - `maxAge` max-age in milliseconds, converted to `expires`
* - `signed` sign the cookie
* - `path` defaults to "/"
*
* Examples:
*
* // "Remember Me" for 15 minutes
* res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
*
* // same as above
* res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
*
* @param {String} name
* @param {String|Object} value
* @param {Object} [options]
* @return {ServerResponse} for chaining
* @public
*/
res.cookie = function (name, value, options) {
2020-11-08 13:54:08 -08:00
const opts = merge({}, options)
const secret = this.req.secret
const signed = opts.signed
2020-10-29 11:14:38 -07:00
if (signed && !secret) {
2020-11-08 13:54:08 -08:00
throw new Error('cookieParser("secret") required for signed cookies')
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
let val = typeof value === 'object'
2020-10-29 11:14:38 -07:00
? 'j:' + JSON.stringify(value)
2020-11-08 13:54:08 -08:00
: String(value)
2020-10-29 11:14:38 -07:00
if (signed) {
2020-11-08 13:54:08 -08:00
val = 's:' + sign(val, secret)
2020-10-29 11:14:38 -07:00
}
if ('maxAge' in opts) {
2020-11-08 13:54:08 -08:00
opts.expires = new Date(Date.now() + opts.maxAge)
opts.maxAge /= 1000
2020-10-29 11:14:38 -07:00
}
if (opts.path == null) {
2020-11-08 13:54:08 -08:00
opts.path = '/'
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
this.append('Set-Cookie', cookie.serialize(name, String(val), opts))
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
return this
}
2020-10-29 11:14:38 -07:00
/**
* Set the location header to `url`.
*
* The given `url` can also be "back", which redirects
* to the _Referrer_ or _Referer_ headers or "/".
*
* Examples:
*
* res.location('/foo/bar').;
* res.location('http://example.com');
* res.location('../login');
*
* @param {String} url
* @return {ServerResponse} for chaining
* @public
*/
2020-11-08 13:54:08 -08:00
res.location = function location (url) {
let loc = url
2020-10-29 11:14:38 -07:00
// "back" is an alias for the referrer
if (url === 'back') {
2020-11-08 13:54:08 -08:00
loc = this.req.get('Referrer') || '/'
2020-10-29 11:14:38 -07:00
}
// set location
2020-11-08 13:54:08 -08:00
return this.set('Location', encodeUrl(loc))
}
2020-10-29 11:14:38 -07:00
/**
* Redirect to the given `url` with optional response `status`
* defaulting to 302.
*
* The resulting `url` is determined by `res.location()`, so
* it will play nicely with mounted apps, relative paths,
* `"back"` etc.
*
* Examples:
*
* res.redirect('/foo/bar');
* res.redirect('http://example.com');
* res.redirect(301, 'http://example.com');
* res.redirect('../login'); // /blog/post/1 -> /blog/login
*
* @public
*/
2020-11-08 13:54:08 -08:00
res.redirect = function redirect (url) {
let address = url
2020-10-29 11:14:38 -07:00
const base = this.req.app.set('base')
if (base && address.startsWith('/')) address = base + address.substr(1)
2020-11-08 13:54:08 -08:00
let body
let status = 302
2020-10-29 11:14:38 -07:00
// allow status / url
if (arguments.length === 2) {
if (typeof arguments[0] === 'number') {
2020-11-08 13:54:08 -08:00
status = arguments[0]
address = arguments[1]
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
deprecate('res.redirect(url, status): Use res.redirect(status, url) instead')
status = arguments[1]
2020-10-29 11:14:38 -07:00
}
}
// Set location header
2020-11-08 13:54:08 -08:00
address = this.location(address).get('Location')
2020-10-29 11:14:38 -07:00
// Support text/{plain,html} by default
this.format({
2020-11-08 13:54:08 -08:00
text: function () {
2020-10-29 11:14:38 -07:00
body = statuses[status] + '. Redirecting to ' + address
},
2020-11-08 13:54:08 -08:00
html: function () {
const u = escapeHtml(address)
2020-10-29 11:14:38 -07:00
body = '<p>' + statuses[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
},
2020-11-08 13:54:08 -08:00
default: function () {
body = ''
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
})
2020-10-29 11:14:38 -07:00
// Respond
2020-11-08 13:54:08 -08:00
this.statusCode = status
this.set('Content-Length', Buffer.byteLength(body))
2020-10-29 11:14:38 -07:00
if (this.req.method === 'HEAD') {
2020-11-08 13:54:08 -08:00
this.end()
2020-10-29 11:14:38 -07:00
} else {
2020-11-08 13:54:08 -08:00
this.end(body)
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
}
2020-10-29 11:14:38 -07:00
/**
* Add `field` to Vary. If already present in the Vary set, then
* this call is simply ignored.
*
* @param {Array|String} field
* @return {ServerResponse} for chaining
* @public
*/
2020-11-08 13:54:08 -08:00
res.vary = function (field) {
2020-10-29 11:14:38 -07:00
// checks for back-compat
if (!field || (Array.isArray(field) && !field.length)) {
2020-11-08 13:54:08 -08:00
deprecate('res.vary(): Provide a field name')
return this
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
vary(this, field)
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
return this
}
2020-10-29 11:14:38 -07:00
/**
* Render `view` with the given `options` and optional callback `fn`.
* When a callback function is given a response will _not_ be made
* automatically, otherwise a response of _200_ and _text/html_ is given.
*
* Options:
*
* - `cache` boolean hinting to the engine it should cache
* - `filename` filename of the view being rendered
*
* @public
*/
2020-11-08 13:54:08 -08:00
res.render = function render (view, options, callback) {
const app = this.req.app
let done = callback
let opts = options || {}
const req = this.req
const self = this
2020-10-29 11:14:38 -07:00
// support callback function as second arg
if (typeof options === 'function') {
2020-11-08 13:54:08 -08:00
done = options
opts = {}
2020-10-29 11:14:38 -07:00
}
// merge res.locals
2020-11-08 13:54:08 -08:00
opts._locals = self.locals
2020-10-29 11:14:38 -07:00
// default callback to respond
done = done || function (err, str) {
2020-11-08 13:54:08 -08:00
if (err) return req.next(err)
self.send(str)
}
2020-10-29 11:14:38 -07:00
// render
2020-11-08 13:54:08 -08:00
app.render(view, opts, done)
}
2020-10-29 11:14:38 -07:00
// pipe the send file stream
2020-11-08 13:54:08 -08:00
function sendfile (res, file, options, callback) {
let done = false
let streaming
2020-10-29 11:14:38 -07:00
// request aborted
2020-11-08 13:54:08 -08:00
function onaborted () {
if (done) return
done = true
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
const err = new Error('Request aborted')
err.code = 'ECONNABORTED'
callback(err)
2020-10-29 11:14:38 -07:00
}
// directory
2020-11-08 13:54:08 -08:00
function ondirectory () {
if (done) return
done = true
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
const err = new Error('EISDIR, read')
err.code = 'EISDIR'
callback(err)
2020-10-29 11:14:38 -07:00
}
// errors
2020-11-08 13:54:08 -08:00
function onerror (err) {
if (done) return
done = true
callback(err)
2020-10-29 11:14:38 -07:00
}
// ended
2020-11-08 13:54:08 -08:00
function onend () {
if (done) return
done = true
callback()
2020-10-29 11:14:38 -07:00
}
// file
2020-11-08 13:54:08 -08:00
function onfile () {
streaming = false
2020-10-29 11:14:38 -07:00
}
// finished
2020-11-08 13:54:08 -08:00
function onfinish (err) {
if (err && err.code === 'ECONNRESET') return onaborted()
if (err) return onerror(err)
if (done) return
2020-10-29 11:14:38 -07:00
setImmediate(function () {
if (streaming !== false && !done) {
2020-11-08 13:54:08 -08:00
onaborted()
return
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
if (done) return
done = true
callback()
})
2020-10-29 11:14:38 -07:00
}
// streaming
2020-11-08 13:54:08 -08:00
function onstream () {
streaming = true
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
file.on('directory', ondirectory)
file.on('end', onend)
file.on('error', onerror)
file.on('file', onfile)
file.on('stream', onstream)
onFinished(res, onfinish)
2020-10-29 11:14:38 -07:00
if (options.headers) {
// set headers on successful transfer
2020-11-08 13:54:08 -08:00
file.on('headers', function headers (res) {
const obj = options.headers
const keys = Object.keys(obj)
2020-10-29 11:14:38 -07:00
2020-11-08 13:54:08 -08:00
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
res.setHeader(k, obj[k])
2020-10-29 11:14:38 -07:00
}
2020-11-08 13:54:08 -08:00
})
2020-10-29 11:14:38 -07:00
}
// pipe
2020-11-08 13:54:08 -08:00
file.pipe(res)
2020-10-29 11:14:38 -07:00
}
/**
* Stringify JSON, like JSON.stringify, but v8 optimized, with the
* ability to escape characters that can trigger HTML sniffing.
*
* @param {*} value
* @param {function} replaces
* @param {number} spaces
* @param {boolean} escape
* @returns {string}
* @private
*/
function stringify (value, replacer, spaces, escape) {
// v8 checks arguments.length for optimizing simple call
// https://bugs.chromium.org/p/v8/issues/detail?id=4730
2020-11-08 13:54:08 -08:00
let json = replacer || spaces
2020-10-29 11:14:38 -07:00
? JSON.stringify(value, replacer, spaces)
2020-11-08 13:54:08 -08:00
: JSON.stringify(value)
2020-10-29 11:14:38 -07:00
if (escape) {
json = json.replace(/[<>&]/g, function (c) {
switch (c.charCodeAt(0)) {
case 0x3c:
return '\\u003c'
case 0x3e:
return '\\u003e'
case 0x26:
return '\\u0026'
/* istanbul ignore next: unreachable default */
default:
return c
}
})
}
return json
}