You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In lib/response.js in res.cookie() some assumptions are made that the incoming maxAge option will always be a number. However, if maxAge is set to undefined through some process, opts.maxAge /= 1000 returns NaN. maxAge should be verified and/or coerced to be numeric.
Yep, makes sense. To throw or to coerce would depend on what happens currently with the various types. Ideally we should throw, but if there are other types currently working or the set-cookie header is still valid then we may just need to coerce for now.
When it is undefined as your description, can you describe what happens? Does it cause a throw currently or does the set-cookie header get set? If it is set, can you paste the value here?
If maxAge = undefined, then opts.expires becomes NaN and so does opts.maxAge. These values are then passed to node_modules/cookie/index.js's serialize function, which throws because these are bad values. Depending on how the code is structured, and where in the promise tree one may be, this could cause no response to ever be returned.
Ok, so it already throws when maxAge is set to a non number? That would seem to me to be the expected behavior. Is it just that the error message thrown is confusing or something?
Throwing is good, and the error is clear, however, there is a dependency on a downstream dependent library to behave correctly instead of Express actually doing the right thing. Express should never be asking for NaN to be set for a cookies expires or maxAge value. Also, it is unexpected that setting a cookie should ever throw when setting maxAge to undefined because that would logically coerce to 0 like null does, thus I didn't wrap my call to res.cookie() in a Try/Catch, so this took me a while to find.
The only symptom I had was Express would never return. This was exspecially strange because, due to a refactor, this value was being set to null, which worked fine since that calculates as 0, and it was changed to undefined, which caused a throw, which is very unexpected from the consumers point of view. The refactor was to be able to turn on Node strictNullsChecks flag.
Gotcha. So thinking a bit more of it, do you think that res.cookie({}) should produce the identical output as res.cookie({ maxAge: undefined })? Accessing .maxAge property on both of these example objects results in undefined, so if currently only the second is throwing, should the second produce the same set-cookie header as the first? Why or why not?
The only problem with that is, the setting of maxAge: undefined is a deliberate action which caused one behavior for maxAge in opts, verses not setting maxAge at all creates different behaviors. I feel like we should handle the case of deliberate consumer action vs. no consumer action as different.
Really, my problem came from thinking that under the hood null and undefined were largely interchangeable, as I would imagine most developers assume.
Perhaps instead of maxAge in opts it should be opts.maxAge != null, which is normally how I would code that as the intention is very clear. I'm actually surprised that there isn't a numeric check here given what we are doing with the date.
Right. The code is from before I working on Express, so cannot answer as to why it was done that way. But that doesn't take away from some of the concerns which is mainly (1) not break current use cases and (2) what should be the right resolution.
The main downside to using 'in' is that it may not indicate explicit user action. For example:
$ node -pe 'Object.prototype.maxAge=undefined; ("maxAge" in {})'
true
So just the property existing on the global prototype makes it return true, even though an empty object is what was passed in by the user (and all the user is aware of passing in).
That is exactly my point, which is why I made the PR the way I did. It doesn't break any current use-cases, but it does causes undefined to behave like null does, which is what a user would expect. It also checks to make sure that a number isn't used, but that breaks Node 0.10, so I'll probably remove that check as it isn't directly related to the problem under discussion here.
It just feels weird. Because if it makes it 0, then the cookie will immediately expire. That change would make there be no possible way to explicitly state that the cookie should be a session length cookie, then?
If the goal is to get null and undefined to behave the same, when we need to change maxAge in opts to maxAge != null. If the goal is to have undefined behave consistently, then we have to use maxAge !== undefined.
It seems if we want to preserve session cookies and the current null behavior, option 2 is our only choice, though it feels like the wrong 'correct' choice as a consumer would encounter different behavior with the both falsey and non-numeric null and undefined.
Is this a breaking change that should go in v.Next?
If it's a breaking change, then yes. If not, we can land in 4.x. We would want to land as much as possible in 4.x, ideally. If there is a breaking change here, we'll want to lay it out what is changing and such so it's documented for when the 4 to 5 migration guide is written.
Technically all of this is a change in behavior, but I think we can probably change maxAge in opts to maxAge !== undefined and fix the specific case of setting undefined and expecting the same behavior as not having set maxAge at all.
My case where I expected null and undefined to behave the same could be argued to be a breaking change to make work, but could also easily be argued that null working is a fluke as we would never want to be adding null to anything or dividing null by anything, and thus is a bug that should be fixed.
Thus the question is: is maxAge being anything but a number and us not handling that case, but relying on JavaScript implementation details, a bug? I would argue that if it wasn't set to a number it probably shouldn't be used at all and the bug is insufficient checks which should be fixed whenever, however, I'll concede to your more seasoned opinion. I probably shouldn't have been passing null or undefined when what I really wanted was 0.
Yes, I mean, fixing a bug is typically always a breaking change in the pure sense. That doesn't mean we don't fix them anyway, of course :) If it's iffy but desirable, they are typically included in minor versions instead of patch versions, but still fixed within the major.
is maxAge being anything but a number and us not handling that case, but relying on JavaScript implementation details, a bug?
So in a way we relay on javascript details for almost everything, since this is javascript. I know that's not what you mean, but just not sure how to really answer that question generically.
For the specific of "should we be dividing a non-number by a number, I would say no, unless there is some special reason, we should above it. That would mean the act of doing null / 1000 etc. is a bug at least in the logic. The second question is how does this logic bug manifest to our users?
Assuming that is passed in doesn't have a special valueOf() defined (like a Date object) the outcome of the division is going to be NaN and even though the cookie module catches that, ideally this module wouldn't pass that down, specifically because it created the value of NaN (not the user).
This would, to me, say everything except numbers and null are basically always going to result in NaN and thus we can just not do the division on the value. The handling of null is the only tricky one, because null actually coerces into the number 0, so maxAge: null results in 0 in Express (but no max age in cookie module).
I would say from that, we have two divisions of changes:
(1) stop dividing all non-numbers and non-null values and let maxAge pass into cookie module as-is (and don't populate expires). This would let cookie module handle the validation and error instead of re-implementing it here. This will still cause things like { maxAge: function () {} } throw, but that feels right to me because it's unsupported to pass a function as the value. I don't think any behavior will change from this, apart from the undefined behavior no longer throwing.
(2) ideally null to Express would just work the same as null to the cookie module for this value, that is it acts like undefined. This is the "tricky" bug because perhaps people are using that to delete cookies, like perhaps (res.cookie('foo', '', { maxAge: null })). Yes, weird, but based on other things changed and then ha to revert in the past I would not be surprised. We should fix this behavior in the 5.0 branch at minimum. Coming back to 4.x it's either (a) do nothing (b) add deprecation warning to null value or (c) change null behavior. I lay those out because I'm undecided on the action on this for 4.x
thank you @cjbarth I have a tiny example of this here. It looks like the initial action to take would be to make a deprecation notice in express 4. Would you like to raise PR for this?
I would like to add that we are working hard to get 5.0 out the door as soon as possible. Unless something critical comes up, 4.18 will be the last minor release of 4.x prior to the final 5.0, which means that for the underlying breaking change to even be considered for 5.0 at this point, we would want to get a 4.x-targeting deprecation ready quickly to get into the 4.18 release. There is always 6.0, and don't let that scare you, either; we have also been at work discussing and then planning after 5.0 in order for 6.0 to not sit anywhere near as long as 5.0 did 😂
This isn't much notice, especially given how long this item has sat dormant. I'll see what I can do about a deprecation notice in the next day or two.
Just to be clear, what we are looking for right now in the 4.x branch is to have a deprecated warning issued if any non-number is passed in.
Then, in the 5.x branch, detect if the value passed in is a non-number, and then avoid division on that number and pass it directly to cookie and let them handle the case; we'd then change the warning from a 'deprecation' warning to just a regular one, since we'd still want to warn that something strange is going on (getting a non-number and expecting an number).
Last night I was just going over this issue again to try and wrap my mind around all the topics we spoke about.
Before I write up my thoughts here, I did want to ask to confirm something. From your POV, what do you believe the goal is? This is just to get back in sync with each other.
I believe at least one goal (maybe the only one?) is that setting maxAge to undefined should produce a cookie header of max-age=0?
If that is the case, I think we need to revisit a little bit, as that is counter to what the underlying cookie module does on that value. Typically in javascript { foo: undefined } and {} both have an undefined foo property.
I believe that a specific undefined should work the same as if the property was not specified.
As for null that one is up for debate, but seems to make to most sense to signal a session-length expiration cookie.
For other non numbers like strings and stuff, those should error out for sure. I believe the underlying module will already do this, but we could likely guard for it higher?
Never perform math on a non-number, including null and undefined
A property being defined as undefined and a property not defined should be treated the same
Behavior for null and undefined should be either treated (or coalesced to be) the same, or be the problem of the downstream library, or other?
The problem is that some of these might be considered a breaking change, though I would argue that really we just have a bug.
The other problem is for 3, what should the correct behavior be? Should they be coalesced to 0, or sent to the downstream library. If they are just sent along, how do we handle the case of undefined being set and undefined resulting from a property not being set. I could see an argument for them being coalesced to 0 because that is the only logical meaning, but OTOH undefined often means to just use the default (which I'd have to refresh my memory of what that is).
I know this has kind of gotten forgotten in the pile of issues, but I am here trying to go through them all :)
So yea, the goals there I think make sense. Basically this module is using maxAge to both add expires and manipulate it to represent the same value in the underlying library (i.e. milliseconds to seconds). As such, it should just use the same logic to determine if maxAge is "there" as the underlying library does -- I updated your PR as such.
This makes it such that maxAge: undefined works the same as any other option, same with null. As on top of that, keeps the same number coercion the underlying library does.
Activity
dougwilson commentedon Apr 18, 2019
Yep, makes sense. To throw or to coerce would depend on what happens currently with the various types. Ideally we should throw, but if there are other types currently working or the set-cookie header is still valid then we may just need to coerce for now.
When it is
undefined
as your description, can you describe what happens? Does it cause a throw currently or does the set-cookie header get set? If it is set, can you paste the value here?maxAge
appropriateness before use #3936cjbarth commentedon Apr 18, 2019
The relavent two lines are:
If
maxAge = undefined
, thenopts.expires
becomesNaN
and so doesopts.maxAge
. These values are then passed tonode_modules/cookie/index.js
'sserialize
function, which throws because these are bad values. Depending on how the code is structured, and where in the promise tree one may be, this could cause no response to ever be returned.dougwilson commentedon Apr 18, 2019
Ok, so it already throws when maxAge is set to a non number? That would seem to me to be the expected behavior. Is it just that the error message thrown is confusing or something?
cjbarth commentedon Apr 18, 2019
Throwing is good, and the error is clear, however, there is a dependency on a downstream dependent library to behave correctly instead of Express actually doing the right thing. Express should never be asking for
NaN
to be set for a cookiesexpires
ormaxAge
value. Also, it is unexpected that setting a cookie should ever throw when settingmaxAge
toundefined
because that would logically coerce to0
likenull
does, thus I didn't wrap my call tores.cookie()
in a Try/Catch, so this took me a while to find.The only symptom I had was Express would never return. This was exspecially strange because, due to a refactor, this value was being set to
null
, which worked fine since that calculates as0
, and it was changed toundefined
, which caused athrow
, which is very unexpected from the consumers point of view. The refactor was to be able to turn on NodestrictNullsChecks
flag.dougwilson commentedon Apr 18, 2019
Gotcha. So thinking a bit more of it, do you think that
res.cookie({})
should produce the identical output asres.cookie({ maxAge: undefined })
? Accessing.maxAge
property on both of these example objects results inundefined
, so if currently only the second is throwing, should the second produce the sameset-cookie
header as the first? Why or why not?cjbarth commentedon Apr 18, 2019
The only problem with that is, the setting of
maxAge: undefined
is a deliberate action which caused one behavior formaxAge in opts
, verses not settingmaxAge
at all creates different behaviors. I feel like we should handle the case of deliberate consumer action vs. no consumer action as different.Really, my problem came from thinking that under the hood
null
andundefined
were largely interchangeable, as I would imagine most developers assume.Perhaps instead of
maxAge in opts
it should beopts.maxAge != null
, which is normally how I would code that as the intention is very clear. I'm actually surprised that there isn't a numeric check here given what we are doing with the date.dougwilson commentedon Apr 18, 2019
Right. The code is from before I working on Express, so cannot answer as to why it was done that way. But that doesn't take away from some of the concerns which is mainly (1) not break current use cases and (2) what should be the right resolution.
dougwilson commentedon Apr 18, 2019
The main downside to using
'in'
is that it may not indicate explicit user action. For example:So just the property existing on the global prototype makes it return
true
, even though an empty object is what was passed in by the user (and all the user is aware of passing in).cjbarth commentedon Apr 18, 2019
That is exactly my point, which is why I made the PR the way I did. It doesn't break any current use-cases, but it does causes
undefined
to behave likenull
does, which is what a user would expect. It also checks to make sure that a number isn't used, but that breaks Node0.10
, so I'll probably remove that check as it isn't directly related to the problem under discussion here.dougwilson commentedon Apr 18, 2019
It just feels weird. Because if it makes it 0, then the cookie will immediately expire. That change would make there be no possible way to explicitly state that the cookie should be a session length cookie, then?
cjbarth commentedon Apr 18, 2019
That is a very good point...
If the goal is to get
null
andundefined
to behave the same, when we need to changemaxAge in opts
tomaxAge != null
. If the goal is to haveundefined
behave consistently, then we have to usemaxAge !== undefined
.It seems if we want to preserve session cookies and the current
null
behavior, option 2 is our only choice, though it feels like the wrong 'correct' choice as a consumer would encounter different behavior with the both falsey and non-numericnull
andundefined
.Is this a breaking change that should go in v.Next?
dougwilson commentedon Apr 18, 2019
If it's a breaking change, then yes. If not, we can land in 4.x. We would want to land as much as possible in 4.x, ideally. If there is a breaking change here, we'll want to lay it out what is changing and such so it's documented for when the 4 to 5 migration guide is written.
cjbarth commentedon Apr 18, 2019
Technically all of this is a change in behavior, but I think we can probably change
maxAge in opts
tomaxAge !== undefined
and fix the specific case of settingundefined
and expecting the same behavior as not having setmaxAge
at all.My case where I expected
null
andundefined
to behave the same could be argued to be a breaking change to make work, but could also easily be argued thatnull
working is a fluke as we would never want to be addingnull
to anything or dividingnull
by anything, and thus is a bug that should be fixed.Thus the question is: is
maxAge
being anything but a number and us not handling that case, but relying on JavaScript implementation details, a bug? I would argue that if it wasn't set to a number it probably shouldn't be used at all and the bug is insufficient checks which should be fixed whenever, however, I'll concede to your more seasoned opinion. I probably shouldn't have been passingnull
orundefined
when what I really wanted was0
.I'll update my PR at your direction.
dougwilson commentedon Apr 18, 2019
Yes, I mean, fixing a bug is typically always a breaking change in the pure sense. That doesn't mean we don't fix them anyway, of course :) If it's iffy but desirable, they are typically included in minor versions instead of patch versions, but still fixed within the major.
So in a way we relay on javascript details for almost everything, since this is javascript. I know that's not what you mean, but just not sure how to really answer that question generically.
For the specific of "should we be dividing a non-number by a number, I would say no, unless there is some special reason, we should above it. That would mean the act of doing
null / 1000
etc. is a bug at least in the logic. The second question is how does this logic bug manifest to our users?Assuming that is passed in doesn't have a special
valueOf()
defined (like aDate
object) the outcome of the division is going to beNaN
and even though thecookie
module catches that, ideally this module wouldn't pass that down, specifically because it created the value ofNaN
(not the user).This would, to me, say everything except numbers and
null
are basically always going to result inNaN
and thus we can just not do the division on the value. The handling ofnull
is the only tricky one, becausenull
actually coerces into the number0
, somaxAge: null
results in 0 in Express (but no max age incookie
module).I would say from that, we have two divisions of changes:
(1) stop dividing all non-numbers and non-null values and let
maxAge
pass intocookie
module as-is (and don't populateexpires
). This would letcookie
module handle the validation and error instead of re-implementing it here. This will still cause things like{ maxAge: function () {} }
throw, but that feels right to me because it's unsupported to pass a function as the value. I don't think any behavior will change from this, apart from theundefined
behavior no longer throwing.(2) ideally
null
to Express would just work the same asnull
to thecookie
module for this value, that is it acts likeundefined
. This is the "tricky" bug because perhaps people are using that to delete cookies, like perhaps (res.cookie('foo', '', { maxAge: null })
). Yes, weird, but based on other things changed and then ha to revert in the past I would not be surprised. We should fix this behavior in the 5.0 branch at minimum. Coming back to 4.x it's either (a) do nothing (b) add deprecation warning tonull
value or (c) changenull
behavior. I lay those out because I'm undecided on the action on this for 4.x7 remaining items
ghinks commentedon Apr 11, 2020
thank you @cjbarth I have a tiny example of this here. It looks like the initial action to take would be to make a deprecation notice in express 4. Would you like to raise PR for this?
dougwilson commentedon Apr 15, 2020
I would like to add that we are working hard to get 5.0 out the door as soon as possible. Unless something critical comes up, 4.18 will be the last minor release of 4.x prior to the final 5.0, which means that for the underlying breaking change to even be considered for 5.0 at this point, we would want to get a 4.x-targeting deprecation ready quickly to get into the 4.18 release. There is always 6.0, and don't let that scare you, either; we have also been at work discussing and then planning after 5.0 in order for 6.0 to not sit anywhere near as long as 5.0 did 😂
cjbarth commentedon Apr 15, 2020
This isn't much notice, especially given how long this item has sat dormant. I'll see what I can do about a deprecation notice in the next day or two.
Just to be clear, what we are looking for right now in the 4.x branch is to have a deprecated warning issued if any non-number is passed in.
Then, in the 5.x branch, detect if the value passed in is a non-number, and then avoid division on that number and pass it directly to
cookie
and let them handle the case; we'd then change the warning from a 'deprecation' warning to just a regular one, since we'd still want to warn that something strange is going on (getting a non-number and expecting an number).Is that what we'd all like to see?
cjbarth commentedon Apr 23, 2020
Just checking again before I proceed; is the above how we'd like to proceed?
dougwilson commentedon Apr 23, 2020
Last night I was just going over this issue again to try and wrap my mind around all the topics we spoke about.
Before I write up my thoughts here, I did want to ask to confirm something. From your POV, what do you believe the goal is? This is just to get back in sync with each other.
I believe at least one goal (maybe the only one?) is that setting maxAge to
undefined
should produce a cookie header ofmax-age=0
?If that is the case, I think we need to revisit a little bit, as that is counter to what the underlying cookie module does on that value. Typically in javascript
{ foo: undefined }
and{}
both have an undefined foo property.I believe that a specific
undefined
should work the same as if the property was not specified.As for
null
that one is up for debate, but seems to make to most sense to signal a session-length expiration cookie.For other non numbers like strings and stuff, those should error out for sure. I believe the underlying module will already do this, but we could likely guard for it higher?
cjbarth commentedon Apr 24, 2020
Goals:
null
andundefined
undefined
and a property not defined should be treated the samenull
andundefined
should be either treated (or coalesced to be) the same, or be the problem of the downstream library, or other?The problem is that some of these might be considered a breaking change, though I would argue that really we just have a bug.
The other problem is for 3, what should the correct behavior be? Should they be coalesced to 0, or sent to the downstream library. If they are just sent along, how do we handle the case of
undefined
being set andundefined
resulting from a property not being set. I could see an argument for them being coalesced to 0 because that is the only logical meaning, but OTOHundefined
often means to just use the default (which I'd have to refresh my memory of what that is).Does that make sense?
dougwilson commentedon Mar 26, 2022
I know this has kind of gotten forgotten in the pile of issues, but I am here trying to go through them all :)
So yea, the goals there I think make sense. Basically this module is using
maxAge
to both addexpires
and manipulate it to represent the same value in the underlying library (i.e. milliseconds to seconds). As such, it should just use the same logic to determine ifmaxAge
is "there" as the underlying library does -- I updated your PR as such.This makes it such that
maxAge: undefined
works the same as any other option, same withnull
. As on top of that, keeps the same number coercion the underlying library does.Fix behavior of null/undefined as "maxAge" in res.cookie
rhodgkins commentedon Apr 13, 2022