fix(res.sendFile): treat the app etag setting as a default, not an override - #7467
NgoQuocViet2001 wants to merge 1 commit 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)
| // wire application etag option to send, as a default only: every other | ||
| // option is passed along to send untouched, and overriding this one made | ||
| // send's documented `etag` option unreachable through res.sendFile |
There was a problem hiding this comment.
I don't think that expanding this comment is necessary. I know that LLMs love comments, but not every line of code needs 3 lines of comments to explain what it does.
| if (opts.etag === undefined) { | ||
| opts.etag = this.app.enabled('etag'); | ||
| } |
There was a problem hiding this comment.
| if (opts.etag === undefined) { | |
| opts.etag = this.app.enabled('etag'); | |
| } | |
| opts.etag ??= this.app.enabled('etag'); |
This would slightly change logic (overwrite undefined and null), but why not write it the easier way (boolean is expected anyway). Nullish coalescing assignment was designed for such use case.
|
Thanks for digging those up. They're all the Agreed on the docs too. The |
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 undefined:
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 stops clobbering the caller's object when they did supply one.Test plan
test/res.sendFile.jsbeside the existingshould disable the ETag function if requested: an explicit{ etag: false }against the default-on app, and an explicit{ etag: true }againstapp.disable('etag').mocha test/→ 1154 passing, no failures. The two pre-existing ETag tests pass noetagoption, so they are unaffected.lib/response.jsfails both new tests.npm run lint→ clean.Note
With no
etagoption supplied, the setting is still written onto the caller's object, as before. Narrowing that further means copyingopts, which felt like a separate change from the behaviour fix — happy to include it if you would rather.