Repository navigation
fix(res.sendFile): treat the app etag setting as a default, not an override - #7467
NgoQuocViet2001 wants to merge 3 commits into
Conversation
…erride expressjs#6073 wired the application etag setting into res.sendFile to close expressjs#2294. The assignment is unconditional, so it also overwrites an etag the caller passed in -- and writes the setting onto the caller's own options object. send documents etag as an option and res.sendFile documents that other options are passed along to it, but this one could not get through: with the app default on, { etag: false } still sent an ETag, and with the app setting disabled, { etag: true } still sent none. Assign only when the caller left it undefined, which keeps expressjs#2294's behaviour for callers that pass no etag.
krzysdz
left a comment
There was a problem hiding this comment.
Unconditionally overwriting the etag option probably isn't the best idea. There is code which uses etag: false with res.sendFile() that would stop working properly in Express 5.
This option probably should also be documented on the website. Currently it lists all options from Express's res.sendFile() and some, but not all that are used by send.
cc @wesleytodd @UlisesGascon (reviewers of #6073)
|
Thanks for digging those up. They're all the Agreed on the docs too. The |
Restore the original one-line comment. An explicit `etag: null` now falls back to the app setting as well, same as undefined.
krzysdz
left a comment
There was a problem hiding this comment.
Oops, this is still slightly wrong. If opts.etag is undefined/null (or does not exist) on the passed options object, this assignment will mutate the options argument and this modification will be visible outside this function.
Before setting the etag option a copy of opts should be made. For example (I can't make a suggestion on unmodified lines):
opts = { ...opts }
// wire application etag option to send
opts.etag ??= this.app.enabled('etag');This will set etag only on a copy of the argument and only if it does not exist or is null/undefined.
colinandres885-boop
left a comment
There was a problem hiding this comment.
- sendFile.[[
2026-q1-quarterly-net-interest-margin.csv]()]()
When the caller passes no etag, the app setting was written onto their own options object. Set it on an object that inherits from it instead, the same way res.download layers its headers. A spread copy would drop the options res.download passes through its prototype (root, dotfiles, maxAge).
|
Thanks, fixed in 80318c4: the default is now set on a copy, and a new test checks that the options object passed to I used |
Problem
#6073 wired the application
etagsetting intores.sendFileto close #2294. The assignment is unconditional:optsis the caller's own options object (var opts = options || {}), so this both overwrites anetagthe caller passed and writes the setting onto their object.senddocumentsetagas an option (opts.etag !== undefined ? Boolean(opts.etag) : true), andres.sendFile's own JSDoc says "Other options are passed along tosend", which every other option does. This one could not get through.Measured over HTTP against
master(5.2.1):res.downloadroutes throughres.sendFile, so it behaves the same way.Fix
Assign only when the caller left it unset, and on a copy of their options:
This keeps #2294's behaviour intact: with no
etagoption,app.disable('etag')still turns it off, which is what that issue asked for ("app.disable('etag')setssendFile()soptions.etagtofalsefor you"). It restores the per-call option that #6073's title advertises, and it no longer writes anything onto the caller's object.Test plan
test/res.sendFile.jsbeside the existingshould disable the ETag function if requested: an explicit{ etag: false }against the default-on app, an explicit{ etag: true }againstapp.disable('etag'), and a check that the options object passed in comes back unchanged.npm test→ 1263 passing, no failures. The two pre-existing ETag tests pass noetagoption, so they are unaffected.lib/response.jsfails the new tests.npm run lint→ clean.Note
The copy uses
Object.create(opts)rather than{ ...opts }.res.downloadpasses its options tores.sendFilethe same way (Object.create(opts)plus its ownheaders), so a spread would drop theroot,dotfiles,maxAgeandetaggiven tores.download. With a spread, 6 existing tests fail (4 intest/res.download.js, 2 in the downloads acceptance test).