diff --git a/lib/response.js b/lib/response.js index 7a2f0ecce56..355b7f7997a 100644 --- a/lib/response.js +++ b/lib/response.js @@ -5,48 +5,48 @@ * MIT Licensed */ -'use strict'; +"use strict"; /** * Module dependencies. * @private */ -var contentDisposition = require('content-disposition'); -var createError = require('http-errors') -var deprecate = require('depd')('express'); -var encodeUrl = require('encodeurl'); -var escapeHtml = require('escape-html'); -var http = require('node:http'); -var onFinished = require('on-finished'); -var mime = require('mime-types') -var path = require('node:path'); -var pathIsAbsolute = require('node:path').isAbsolute; -var statuses = require('statuses') -var sign = require('cookie-signature').sign; -var normalizeType = require('./utils').normalizeType; -var normalizeTypes = require('./utils').normalizeTypes; -var setCharset = require('./utils').setCharset; -var cookie = require('cookie'); -var send = require('send'); +var contentDisposition = require("content-disposition"); +var createError = require("http-errors"); +var deprecate = require("depd")("express"); +var encodeUrl = require("encodeurl"); +var escapeHtml = require("escape-html"); +var http = require("node:http"); +var onFinished = require("on-finished"); +var mime = require("mime-types"); +var path = require("node:path"); +var pathIsAbsolute = require("node:path").isAbsolute; +var statuses = require("statuses"); +var sign = require("cookie-signature").sign; +var normalizeType = require("./utils").normalizeType; +var normalizeTypes = require("./utils").normalizeTypes; +var setCharset = require("./utils").setCharset; +var cookie = require("cookie"); +var send = require("send"); var extname = path.extname; var resolve = path.resolve; -var vary = require('vary'); -const { Buffer } = require('node:buffer'); +var vary = require("vary"); +const { Buffer } = require("node:buffer"); /** * Response prototype. * @public */ -var res = Object.create(http.ServerResponse.prototype) +var res = Object.create(http.ServerResponse.prototype); /** * Module exports. * @public */ -module.exports = res +module.exports = res; /** * Set the HTTP status code for the response. @@ -64,11 +64,19 @@ module.exports = res res.status = function status(code) { // Check if the status code is not an integer if (!Number.isInteger(code)) { - throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); + throw new TypeError( + `Invalid status code: ${JSON.stringify( + code + )}. Status code must be an integer.` + ); } // Check if the status code is outside of Node's valid range if (code < 100 || code > 999) { - throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); + throw new RangeError( + `Invalid status code: ${JSON.stringify( + code + )}. Status code must be greater than 99 and less than 1000.` + ); } this.statusCode = code; @@ -94,19 +102,27 @@ res.status = function status(code) { * @public */ -res.links = function(links) { - var link = this.get('Link') || ''; - if (link) link += ', '; - return this.set('Link', link + Object.keys(links).map(function(rel) { - // Allow multiple links if links[rel] is an array - if (Array.isArray(links[rel])) { - return links[rel].map(function (singleLink) { - return `<${singleLink}>; rel="${rel}"`; - }).join(', '); - } else { - return `<${links[rel]}>; rel="${rel}"`; - } - }).join(', ')); +res.links = function (links) { + var link = this.get("Link") || ""; + if (link) link += ", "; + return this.set( + "Link", + link + + Object.keys(links) + .map(function (rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel] + .map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }) + .join(", "); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } + }) + .join(", ") + ); }; /** @@ -133,19 +149,19 @@ res.send = function send(body) { switch (typeof chunk) { // string defaulting to html - case 'string': - if (!this.get('Content-Type')) { - this.type('html'); + case "string": + if (!this.get("Content-Type")) { + this.type("html"); } break; - case 'boolean': - case 'number': - case 'object': + case "boolean": + case "number": + case "object": if (chunk === null) { - chunk = ''; + chunk = ""; } else if (ArrayBuffer.isView(chunk)) { - if (!this.get('Content-Type')) { - this.type('bin'); + if (!this.get("Content-Type")) { + this.type("bin"); } } else { return this.json(chunk); @@ -154,44 +170,44 @@ res.send = function send(body) { } // write strings in utf-8 - if (typeof chunk === 'string') { - encoding = 'utf8'; - type = this.get('Content-Type'); + if (typeof chunk === "string") { + encoding = "utf8"; + type = this.get("Content-Type"); // reflect this in content-type - if (typeof type === 'string') { - this.set('Content-Type', setCharset(type, 'utf-8')); + if (typeof type === "string") { + this.set("Content-Type", setCharset(type, "utf-8")); } } // determine if ETag should be generated - var etagFn = app.get('etag fn') - var generateETag = !this.get('ETag') && typeof etagFn === 'function' + var etagFn = app.get("etag fn"); + var generateETag = !this.get("ETag") && typeof etagFn === "function"; // populate Content-Length - var len + var len; if (chunk !== undefined) { if (Buffer.isBuffer(chunk)) { // get length of Buffer - len = chunk.length + len = chunk.length; } else if (!generateETag && chunk.length < 1000) { // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding) + len = Buffer.byteLength(chunk, encoding); } else { // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding) + chunk = Buffer.from(chunk, encoding); encoding = undefined; - len = chunk.length + len = chunk.length; } - this.set('Content-Length', len); + this.set("Content-Length", len); } // populate ETag var etag; if (generateETag && len !== undefined) { if ((etag = etagFn(chunk, encoding))) { - this.set('ETag', etag); + this.set("ETag", etag); } } @@ -200,20 +216,20 @@ res.send = function send(body) { // strip irrelevant headers if (204 === this.statusCode || 304 === this.statusCode) { - this.removeHeader('Content-Type'); - this.removeHeader('Content-Length'); - this.removeHeader('Transfer-Encoding'); - chunk = ''; + this.removeHeader("Content-Type"); + this.removeHeader("Content-Length"); + this.removeHeader("Transfer-Encoding"); + chunk = ""; } // alter headers for 205 if (this.statusCode === 205) { - this.set('Content-Length', '0') - this.removeHeader('Transfer-Encoding') - chunk = '' + this.set("Content-Length", "0"); + this.removeHeader("Transfer-Encoding"); + chunk = ""; } - if (req.method === 'HEAD') { + if (req.method === "HEAD") { // skip body for HEAD this.end(); } else { @@ -239,14 +255,14 @@ res.send = function send(body) { res.json = function json(obj) { // settings var app = this.app; - var escape = app.get('json escape') - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape) + var escape = app.get("json escape"); + var replacer = app.get("json replacer"); + var spaces = app.get("json spaces"); + var body = stringify(obj, replacer, spaces, escape); // content-type - if (!this.get('Content-Type')) { - this.set('Content-Type', 'application/json'); + if (!this.get("Content-Type")) { + this.set("Content-Type", "application/json"); } return this.send(body); @@ -267,16 +283,16 @@ res.json = function json(obj) { res.jsonp = function jsonp(obj) { // settings var app = this.app; - var escape = app.get('json escape') - var replacer = app.get('json replacer'); - var spaces = app.get('json spaces'); - var body = stringify(obj, replacer, spaces, escape) - var callback = this.req.query[app.get('jsonp callback name')]; + var escape = app.get("json escape"); + var replacer = app.get("json replacer"); + var spaces = app.get("json spaces"); + var body = stringify(obj, replacer, spaces, escape); + var callback = this.req.query[app.get("jsonp callback name")]; // content-type - if (!this.get('Content-Type')) { - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'application/json'); + if (!this.get("Content-Type")) { + this.set("X-Content-Type-Options", "nosniff"); + this.set("Content-Type", "application/json"); } // fixup callback @@ -285,26 +301,31 @@ res.jsonp = function jsonp(obj) { } // jsonp - if (typeof callback === 'string' && callback.length !== 0) { - this.set('X-Content-Type-Options', 'nosniff'); - this.set('Content-Type', 'text/javascript'); + if (typeof callback === "string" && callback.length !== 0) { + this.set("X-Content-Type-Options", "nosniff"); + this.set("Content-Type", "text/javascript"); // restrict callback charset - callback = callback.replace(/[^\[\]\w$.]/g, ''); + callback = callback.replace(/[^\[\]\w$.]/g, ""); if (body === undefined) { // empty argument - body = '' - } else if (typeof body === 'string') { + body = ""; + } else if (typeof body === "string") { // replace chars not allowed in JavaScript that are in JSON - body = body - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029') + body = body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise - body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');'; + body = + "/**/ typeof " + + callback + + " === 'function' && " + + callback + + "(" + + body + + ");"; } return this.send(body); @@ -326,10 +347,10 @@ res.jsonp = function jsonp(obj) { */ res.sendStatus = function sendStatus(statusCode) { - var body = statuses.message[statusCode] || String(statusCode) + var body = statuses.message[statusCode] || String(statusCode); this.status(statusCode); - this.type('txt'); + this.type("txt"); return this.send(body); }; @@ -383,37 +404,39 @@ res.sendFile = function sendFile(path, options, callback) { var opts = options || {}; if (!path) { - throw new TypeError('path argument is required to res.sendFile'); + throw new TypeError("path argument is required to res.sendFile"); } - if (typeof path !== 'string') { - throw new TypeError('path must be a string to res.sendFile') + if (typeof path !== "string") { + throw new TypeError("path must be a string to res.sendFile"); } // support function as second arg - if (typeof options === 'function') { + if (typeof options === "function") { done = options; opts = {}; } if (!opts.root && !pathIsAbsolute(path)) { - throw new TypeError('path must be absolute or specify root to res.sendFile'); + throw new TypeError( + "path must be absolute or specify root to res.sendFile" + ); } // create file stream var pathname = encodeURI(path); // wire application etag option to send - opts.etag = this.app.enabled('etag'); + opts.etag = this.app.enabled("etag"); var file = send(req, pathname, opts); // transfer sendfile(res, file, opts, function (err) { if (done) return done(err); - if (err && err.code === 'EISDIR') return next(); + if (err && err.code === "EISDIR") return next(); // next() all but write errors - if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { + if (err && err.code !== "ECONNABORTED" && err.syscall !== "write") { next(err); } }); @@ -437,55 +460,55 @@ res.sendFile = function sendFile(path, options, callback) { * @public */ -res.download = function download (path, filename, options, callback) { +res.download = function download(path, filename, options, callback) { var done = callback; var name = filename; - var opts = options || null + var opts = options || null; // support function as second or third arg - if (typeof filename === 'function') { + if (typeof filename === "function") { done = filename; name = null; - opts = null - } else if (typeof options === 'function') { - done = options - opts = null + opts = null; + } else if (typeof options === "function") { + done = options; + opts = null; } // support optional filename, where options may be in it's place - if (typeof filename === 'object' && - (typeof options === 'function' || options === undefined)) { - name = null - opts = filename + if ( + typeof filename === "object" && + (typeof options === "function" || options === undefined) + ) { + name = null; + opts = filename; } // set Content-Disposition when file is sent var headers = { - 'Content-Disposition': contentDisposition(name || path) + "Content-Disposition": contentDisposition(name || path), }; // merge user-provided headers if (opts && opts.headers) { - var keys = Object.keys(opts.headers) + var keys = Object.keys(opts.headers); for (var i = 0; i < keys.length; i++) { - var key = keys[i] - if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key] + var key = keys[i]; + if (key.toLowerCase() !== "content-disposition") { + headers[key] = opts.headers[key]; } } } // merge user-provided options - opts = Object.create(opts) - opts.headers = headers + opts = Object.create(opts); + opts.headers = headers; // Resolve the full path for sendFile - var fullPath = !opts.root - ? resolve(path) - : path + var fullPath = !opts.root ? resolve(path) : path; // send file - return this.sendFile(fullPath, opts, done) + return this.sendFile(fullPath, opts, done); }; /** @@ -507,13 +530,13 @@ res.download = function download (path, filename, options, callback) { * @public */ -res.contentType = -res.type = function contentType(type) { - var ct = type.indexOf('/') === -1 - ? (mime.contentType(type) || 'application/octet-stream') - : type; +res.contentType = res.type = function contentType(type) { + var ct = + type.indexOf("/") === -1 + ? mime.contentType(type) || "application/octet-stream" + : type; - return this.set('Content-Type', ct); + return this.set("Content-Type", ct); }; /** @@ -573,28 +596,31 @@ res.type = function contentType(type) { * @public */ -res.format = function(obj){ +res.format = function (obj) { var req = this.req; var next = req.next; - var keys = Object.keys(obj) - .filter(function (v) { return v !== 'default' }) + var keys = Object.keys(obj).filter(function (v) { + return v !== "default"; + }); - var key = keys.length > 0 - ? req.accepts(keys) - : false; + var key = keys.length > 0 ? req.accepts(keys) : false; this.vary("Accept"); if (key) { - this.set('Content-Type', normalizeType(key).value); + this.set("Content-Type", normalizeType(key).value); obj[key](req, this, next); } else if (obj.default) { - obj.default(req, this, next) + obj.default(req, this, next); } else { - next(createError(406, { - types: normalizeTypes(keys).map(function (o) { return o.value }) - })) + next( + createError(406, { + types: normalizeTypes(keys).map(function (o) { + return o.value; + }), + }) + ); } return this; @@ -613,7 +639,7 @@ res.attachment = function attachment(filename) { this.type(extname(filename)); } - this.set('Content-Disposition', contentDisposition(filename)); + this.set("Content-Disposition", contentDisposition(filename)); return this; }; @@ -639,9 +665,11 @@ res.append = function append(field, val) { if (prev) { // concat the new and prev vals - value = Array.isArray(prev) ? prev.concat(val) - : Array.isArray(val) ? [prev].concat(val) - : [prev, val] + value = Array.isArray(prev) + ? prev.concat(val) + : Array.isArray(val) + ? [prev].concat(val) + : [prev, val]; } return this.set(field, value); @@ -668,19 +696,16 @@ res.append = function append(field, val) { * @public */ -res.set = -res.header = function header(field, val) { +res.set = res.header = function header(field, val) { if (arguments.length === 2) { - var value = Array.isArray(val) - ? val.map(String) - : String(val); + var value = Array.isArray(val) ? val.map(String) : String(val); // add charset to content-type - if (field.toLowerCase() === 'content-type') { + if (field.toLowerCase() === "content-type") { if (Array.isArray(value)) { - throw new TypeError('Content-Type cannot be set to an Array'); + throw new TypeError("Content-Type cannot be set to an Array"); } - value = mime.contentType(value) + value = mime.contentType(value); } this.setHeader(field, value); @@ -700,7 +725,7 @@ res.header = function header(field, val) { * @public */ -res.get = function(field){ +res.get = function (field) { return this.getHeader(field); }; @@ -715,11 +740,11 @@ res.get = function(field){ res.clearCookie = function clearCookie(name, options) { // Force cookie expiration by setting expires to the past - const opts = { path: '/', ...options, expires: new Date(1)}; + const opts = { path: "/", ...options, expires: new Date(1) }; // ensure maxAge is not passed - delete opts.maxAge + delete opts.maxAge; - return this.cookie(name, '', opts); + return this.cookie(name, "", opts); }; /** @@ -755,28 +780,27 @@ res.cookie = function (name, value, options) { throw new Error('cookieParser("secret") required for signed cookies'); } - var val = typeof value === 'object' - ? 'j:' + JSON.stringify(value) - : String(value); + var val = + typeof value === "object" ? "j:" + JSON.stringify(value) : String(value); if (signed) { - val = 's:' + sign(val, secret); + val = "s:" + sign(val, secret); } if (opts.maxAge != null) { - var maxAge = opts.maxAge - 0 + var maxAge = opts.maxAge - 0; if (!isNaN(maxAge)) { - opts.expires = new Date(Date.now() + maxAge) - opts.maxAge = Math.floor(maxAge / 1000) + opts.expires = new Date(Date.now() + maxAge); + opts.maxAge = Math.floor(maxAge / 1000); } } if (opts.path == null) { - opts.path = '/'; + opts.path = "/"; } - this.append('Set-Cookie', cookie.serialize(name, String(val), opts)); + this.append("Set-Cookie", cookie.serialize(name, String(val), opts)); return this; }; @@ -799,7 +823,13 @@ res.cookie = function (name, value, options) { */ res.location = function location(url) { - return this.set('Location', encodeUrl(url)); + if (url instanceof URL) { + url = url.toString(); + } else if (typeof url !== "string") { + url = String(url); + } + + return this.set("Location", encodeUrl(url)); }; /** @@ -823,46 +853,47 @@ res.redirect = function redirect(url) { // allow status / url if (arguments.length === 2) { - status = arguments[0] - address = arguments[1] + status = arguments[0]; + address = arguments[1]; } if (!address) { - deprecate('Provide a url argument'); + deprecate("Provide a url argument"); } - if (typeof address !== 'string') { - deprecate('Url must be a string'); + if (typeof address !== "string") { + deprecate("Url must be a string"); } - if (typeof status !== 'number') { - deprecate('Status must be a number'); + if (typeof status !== "number") { + deprecate("Status must be a number"); } // Set location header - address = this.location(address).get('Location'); + address = this.location(address).get("Location"); // Support text/{plain,html} by default this.format({ - text: function(){ - body = statuses.message[status] + '. Redirecting to ' + address + text: function () { + body = statuses.message[status] + ". Redirecting to " + address; }, - html: function(){ + html: function () { var u = escapeHtml(address); - body = '
' + statuses.message[status] + '. Redirecting to ' + u + '
' + body = + "" + statuses.message[status] + ". Redirecting to " + u + "
"; }, - default: function(){ - body = ''; - } + default: function () { + body = ""; + }, }); // Respond this.status(status); - this.set('Content-Length', Buffer.byteLength(body)); + this.set("Content-Length", Buffer.byteLength(body)); - if (this.req.method === 'HEAD') { + if (this.req.method === "HEAD") { this.end(); } else { this.end(body); @@ -878,7 +909,7 @@ res.redirect = function redirect(url) { * @public */ -res.vary = function(field){ +res.vary = function (field) { vary(this, field); return this; @@ -905,7 +936,7 @@ res.render = function render(view, options, callback) { var self = this; // support callback function as second arg - if (typeof options === 'function') { + if (typeof options === "function") { done = options; opts = {}; } @@ -914,10 +945,12 @@ res.render = function render(view, options, callback) { opts._locals = self.locals; // default callback to respond - done = done || function (err, str) { - if (err) return req.next(err); - self.send(str); - }; + done = + done || + function (err, str) { + if (err) return req.next(err); + self.send(str); + }; // render app.render(view, opts, done); @@ -933,8 +966,8 @@ function sendfile(res, file, options, callback) { if (done) return; done = true; - var err = new Error('Request aborted'); - err.code = 'ECONNABORTED'; + var err = new Error("Request aborted"); + err.code = "ECONNABORTED"; callback(err); } @@ -943,8 +976,8 @@ function sendfile(res, file, options, callback) { if (done) return; done = true; - var err = new Error('EISDIR, read'); - err.code = 'EISDIR'; + var err = new Error("EISDIR, read"); + err.code = "EISDIR"; callback(err); } @@ -969,7 +1002,7 @@ function sendfile(res, file, options, callback) { // finished function onfinish(err) { - if (err && err.code === 'ECONNRESET') return onaborted(); + if (err && err.code === "ECONNRESET") return onaborted(); if (err) return onerror(err); if (done) return; @@ -990,16 +1023,16 @@ function sendfile(res, file, options, callback) { streaming = true; } - file.on('directory', ondirectory); - file.on('end', onend); - file.on('error', onerror); - file.on('file', onfile); - file.on('stream', onstream); + file.on("directory", ondirectory); + file.on("end", onend); + file.on("error", onerror); + file.on("file", onfile); + file.on("stream", onstream); onFinished(res, onfinish); if (options.headers) { // set headers on successful transfer - file.on('headers', function headers(res) { + file.on("headers", function headers(res) { var obj = options.headers; var keys = Object.keys(obj); @@ -1026,28 +1059,29 @@ function sendfile(res, file, options, callback) { * @private */ -function stringify (value, replacer, spaces, escape) { +function stringify(value, replacer, spaces, escape) { // v8 checks arguments.length for optimizing simple call // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - var json = replacer || spaces - ? JSON.stringify(value, replacer, spaces) - : JSON.stringify(value); + var json = + replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); - if (escape && typeof json === 'string') { + if (escape && typeof json === "string") { json = json.replace(/[<>&]/g, function (c) { switch (c.charCodeAt(0)) { case 0x3c: - return '\\u003c' + return "\\u003c"; case 0x3e: - return '\\u003e' + return "\\u003e"; case 0x26: - return '\\u0026' + return "\\u0026"; /* istanbul ignore next: unreachable default */ default: - return c + return c; } - }) + }); } - return json + return json; }