diff --git a/lib/request.js b/lib/request.js index 1eb7f9ca16a..185842fa917 100644 --- a/lib/request.js +++ b/lib/request.js @@ -228,16 +228,21 @@ req.range = function range(size, options) { */ defineGetter(req, 'query', function query(){ + if (this._query !== undefined) { + return this._query; + } + var queryparse = this.app.get('query parser fn'); if (!queryparse) { // parsing is disabled - return Object.create(null); + this._query = Object.create(null); + } else { + var querystring = parse(this).query; + this._query = queryparse(querystring); } - var querystring = parse(this).query; - - return queryparse(querystring); + return this._query; }); /** @@ -295,23 +300,29 @@ req.is = function is(types) { */ defineGetter(req, 'protocol', function protocol(){ + if (this._protocol !== undefined) { + return this._protocol; + } + var proto = this.socket.encrypted ? 'https' : 'http'; var trust = this.app.get('trust proxy fn'); if (!trust(this.socket.remoteAddress, 0)) { - return proto; - } + this._protocol = proto; + } else { + // Note: X-Forwarded-Proto is normally only ever a + // single value, but this is to be safe. + var header = this.get('X-Forwarded-Proto') || proto + var index = header.indexOf(',') - // Note: X-Forwarded-Proto is normally only ever a - // single value, but this is to be safe. - var header = this.get('X-Forwarded-Proto') || proto - var index = header.indexOf(',') + this._protocol = index !== -1 + ? header.substring(0, index).trim() + : header.trim(); + } - return index !== -1 - ? header.substring(0, index).trim() - : header.trim() + return this._protocol; }); /** @@ -338,8 +349,12 @@ defineGetter(req, 'secure', function secure(){ */ defineGetter(req, 'ip', function ip(){ + if (this._ip !== undefined) { + return this._ip; + } var trust = this.app.get('trust proxy fn'); - return proxyaddr(this, trust); + this._ip = proxyaddr(this, trust); + return this._ip; }); /** @@ -355,6 +370,9 @@ defineGetter(req, 'ip', function ip(){ */ defineGetter(req, 'ips', function ips() { + if (this._ips !== undefined) { + return this._ips; + } var trust = this.app.get('trust proxy fn'); var addrs = proxyaddr.all(this, trust); @@ -362,7 +380,8 @@ defineGetter(req, 'ips', function ips() { // and remove socket address addrs.reverse().pop() - return addrs + this._ips = addrs; + return this._ips; }); /** @@ -381,16 +400,23 @@ defineGetter(req, 'ips', function ips() { */ defineGetter(req, 'subdomains', function subdomains() { + if (this._subdomains !== undefined) { + return this._subdomains; + } var hostname = this.hostname; - if (!hostname) return []; + if (!hostname) { + this._subdomains = []; + } else { + var offset = this.app.get('subdomain offset'); + var subdomains = !isIP(hostname) + ? hostname.split('.').reverse() + : [hostname]; - var offset = this.app.get('subdomain offset'); - var subdomains = !isIP(hostname) - ? hostname.split('.').reverse() - : [hostname]; + this._subdomains = subdomains.slice(offset); + } - return subdomains.slice(offset); + return this._subdomains; }); /** @@ -401,7 +427,11 @@ defineGetter(req, 'subdomains', function subdomains() { */ defineGetter(req, 'path', function path() { - return parse(this).pathname; + if (this._path !== undefined) { + return this._path; + } + this._path = parse(this).pathname; + return this._path; }); /** @@ -416,6 +446,9 @@ defineGetter(req, 'path', function path() { */ defineGetter(req, 'host', function host(){ + if (this._host !== undefined) { + return this._host; + } var trust = this.app.get('trust proxy fn'); var val = this.get('X-Forwarded-Host'); @@ -427,7 +460,8 @@ defineGetter(req, 'host', function host(){ val = val.substring(0, val.indexOf(',')).trimEnd() } - return val || undefined; + this._host = val || undefined; + return this._host; }); /** @@ -442,19 +476,26 @@ defineGetter(req, 'host', function host(){ */ defineGetter(req, 'hostname', function hostname(){ + if (this._hostname !== undefined) { + return this._hostname; + } var host = this.host; - if (!host) return; - - // IPv6 literal support - var offset = host[0] === '[' - ? host.indexOf(']') + 1 - : 0; - var index = host.indexOf(':', offset); + if (!host) { + this._hostname = undefined; + } else { + // IPv6 literal support + var offset = host[0] === '[' + ? host.indexOf(']') + 1 + : 0; + var index = host.indexOf(':', offset); + + this._hostname = index !== -1 + ? host.substring(0, index) + : host; + } - return index !== -1 - ? host.substring(0, index) - : host; + return this._hostname; }); /** @@ -506,8 +547,12 @@ defineGetter(req, 'stale', function stale(){ */ defineGetter(req, 'xhr', function xhr(){ + if (this._xhr !== undefined) { + return this._xhr; + } var val = this.get('X-Requested-With') || ''; - return val.toLowerCase() === 'xmlhttprequest'; + this._xhr = val.toLowerCase() === 'xmlhttprequest'; + return this._xhr; }); /** diff --git a/lib/response.js b/lib/response.js index b4755a5c060..7795c821b01 100644 --- a/lib/response.js +++ b/lib/response.js @@ -156,6 +156,20 @@ res.send = function send(body) { return this.json(chunk); } break; + case 'bigint': + chunk = chunk.toString(); + encoding = 'utf8'; + const ct = this.get('Content-Type'); + if (typeof ct === 'string') { + this.set('Content-Type', setCharset(ct, 'utf-8')); + } else { + this.type('txt'); + } + break; + case 'symbol': + throw new TypeError('symbol cannot be sent in a response'); + case 'function': + throw new TypeError('function cannot be sent in a response'); } // determine if ETag should be generated @@ -169,8 +183,11 @@ res.send = function send(body) { 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 + } else if (!generateETag) { + // just calculate length when no ETag + len = Buffer.byteLength(chunk, encoding) + } else if (chunk.length < 1000) { + // just calculate length when ETag + small chunk len = Buffer.byteLength(chunk, encoding) } else { // convert chunk to Buffer and calculate @@ -760,7 +777,9 @@ res.cookie = function (name, value, options) { } if (opts.maxAge != null) { - var maxAge = opts.maxAge - 0 + var maxAge = typeof opts.maxAge === 'bigint' + ? Number(opts.maxAge) + : opts.maxAge - 0 if (!isNaN(maxAge)) { opts.expires = new Date(Date.now() + maxAge) @@ -780,12 +799,9 @@ res.cookie = function (name, value, options) { /** * 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('/foo/bar'); * res.location('http://example.com'); * res.location('../login'); * @@ -839,15 +855,16 @@ res.redirect = function redirect(url) { address = this.location(address).get('Location'); // Support text/{plain,html} by default + var statusMessage = statuses.message[status] || 'Redirecting'; this.format({ text: function(){ - body = statuses.message[status] + '. Redirecting to ' + address + body = statusMessage + '. Redirecting to ' + address }, html: function(){ var u = escapeHtml(address); - body = '
' + statuses.message[status] + '. Redirecting to ' + u + '
' + body = '' + statusMessage + '. Redirecting to ' + u + '
' }, default: function(){ diff --git a/test/app.request.js b/test/app.request.js index b6c00f5baa3..285c7311372 100644 --- a/test/app.request.js +++ b/test/app.request.js @@ -139,5 +139,34 @@ describe('app', function(){ .get('/sub/foo') .expect(200, 'tobi', cb) }) + + describe('getter caching', function () { + it('should cache lazy getters per request', function (done) { + var app = express() + + app.use(function (req, res) { + var q1 = req.query + var q2 = req.query + var ips1 = req.ips + var ips2 = req.ips + var path1 = req.path + var path2 = req.path + var sub1 = req.subdomains + var sub2 = req.subdomains + + var assert = require('node:assert') + assert.strictEqual(q1, q2) + assert.strictEqual(ips1, ips2) + assert.strictEqual(path1, path2) + assert.strictEqual(sub1, sub2) + + res.send('cached') + }) + + request(app) + .get('/foo?bar=1') + .expect(200, 'cached', done) + }) + }) }) }) diff --git a/test/res.cookie.js b/test/res.cookie.js index 180d1be3452..161fb6f2bcf 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -183,6 +183,21 @@ describe('res', function(){ .get('/') .expect(500, /option maxAge is invalid/, done) }) + + it('should support bigint maxAge', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: 1000n }) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect('Set-Cookie', /Max-Age=1/) + .end(done) + }) }) describe('priority', function () { diff --git a/test/res.redirect.js b/test/res.redirect.js index 8d2b164e4a2..141e583efb8 100644 --- a/test/res.redirect.js +++ b/test/res.redirect.js @@ -211,4 +211,21 @@ describe('res', function(){ .end(done) }) }) + + describe('when status is custom or non-standard', function(){ + it('should use "Redirecting" fallback in response body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.redirect(399, 'http://google.com'); + }); + + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect(399) + .expect('Location', 'http://google.com') + .expect('Redirecting. Redirecting to http://google.com', done); + }) + }) }) diff --git a/test/res.send.js b/test/res.send.js index f38ffde9776..5702c6e02a1 100644 --- a/test/res.send.js +++ b/test/res.send.js @@ -617,4 +617,33 @@ describe('res', function(){ }) }); }) + + describe('when chunk is bigint', function(){ + it('should send the string representation of bigint', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(123n); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, '123', done); + }) + }) + + describe('when chunk is symbol', function(){ + it('should throw TypeError', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(Symbol('foo')); + }); + + request(app) + .get('/') + .expect(500, done); + }) + }) })