diff --git a/lib/response.js b/lib/response.js index 0d5a4ae4d4d..082712f7d74 100644 --- a/lib/response.js +++ b/lib/response.js @@ -907,8 +907,8 @@ res.render = function render(view, options, callback) { opts = {}; } - // merge res.locals - opts._locals = self.locals; + // merge res.locals (into a copy, so the caller's options are not mutated) + opts = { ...opts, _locals: self.locals }; // default callback to respond done = done || function (err, str) { diff --git a/test/res.render.js b/test/res.render.js index 114b398e0b4..965575db1d3 100644 --- a/test/res.render.js +++ b/test/res.render.js @@ -1,5 +1,6 @@ 'use strict' +var assert = require('node:assert') var express = require('..'); var path = require('node:path') var request = require('supertest'); @@ -295,6 +296,24 @@ describe('res', function(){ .get('/') .expect('

jane

', done); }) + + it('should not mutate the options object', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + var options = { user: { name: 'tobi' } }; + var optionsCopy = { ...options }; + + app.use(function(req, res){ + res.locals.foo = 'bar'; + res.render('user.tmpl', options); + }); + + request(app) + .get('/') + .expect(() => assert.deepStrictEqual(options, optionsCopy)) + .end(done) + }) }) describe('.render(name, options, fn)', function(){