nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
105.25k stars 28.51k forks source link

url: deleting properties has no effect #1591

Closed silverwind closed 9 years ago

silverwind commented 9 years ago

Looks like deleting properties no longer has an effect because the properties are getters/setters. Encountered in https://github.com/npm/npm/blob/master/lib/config/nerf-dart.js

The question is, do we want to/can support this with getters/setters at all, or are we fine with it breaking? If we want to break it, we propable need to patch npm to use uri.prop = null or uri.prop = '' instead of delete uri.prop.

1.8.1

> uri = url.parse("https://registry.lvh.me:8661/")
> delete uri.protocol
> uri.format()
'//registry.lvh.me:8661/'

2.0.0

> uri = url.parse("https://registry.lvh.me:8661/")
> delete uri.protocol
> uri.format()
'https://registry.lvh.me:8661/'

cc: @petkaantonov @domenic @rvagg @othiym23

petkaantonov commented 9 years ago

there is no real utility in deleting like this, uri.protocol = null; etc has same effect (and is more efficient to boot), even with plain objects.

silverwind commented 9 years ago

Yeah, delete perf is horrible, I'm all for not supporting it anymore. What do you think about setting configurable = false on all props to stop delete from deleting the getters/setters?

rvagg commented 9 years ago

I'm pretty sure delete url.prop is in fairly high usage in userland, if we can support this then we probably should because otherwise we're going to be dealing with lots of bug reports

silverwind commented 9 years ago

I don't think this can be fixed without a fundamental change in the module (and likely a big perf loss).

My first idea was to set configurable: false on all props in the hopes it will throw an error on delete, but that just silently fails. It does return false but I guess few check the return value of delete.

monsanto commented 9 years ago

@silverwind it will throw an error in strict mode (at least the case you linked to, it wont for things on the prototype unfortunately)

silverwind commented 9 years ago

@monsanto thanks, I incidentally just read about that. Won't help as these properties are all on the prototype.

monsanto commented 9 years ago

How slow would it be to add the getters on the instance itself for a few releases to help people migrate? Declare the functions themselves outside of the constructor so we don't get into bad closures-and-hidden-classes territory, of course.

Of course it wouldn't help for people who don't use strict mode...

petkaantonov commented 9 years ago

property definition is absurdly slow even if you have static functions

silverwind commented 9 years ago

Hmm I think Object.observe could catch that deletion and set the prop to null.

silverwind commented 9 years ago

Nope, Object.observe won't work on the prototype. :cry:

domenic commented 9 years ago

It's a breaking release, it's ok to break delete. Just do what browsers do (normal, configurable props on the prototype).

Fishrock123 commented 9 years ago

What's the use-case for deleting a prop? Removing that data? That is a terrible way of doing it to begin with.

chrisdickinson commented 9 years ago

@Fishrock123 Yep, removing the data. Using delete probably makes more sense than "assign to false-y" when the aim is to remove a given property from consideration. I'm somewhat ambivalent about removing the ability to delete properties from parsed url objects. At the very least we should note it (loudly and clearly) in the docs.

rvagg commented 9 years ago

@iojs/tc I'd like to hear from other TC members on this, we're about to ship code that will silently break usage like delete url.auth.

I don't believe this to be a simple matter of "assign to null noob, it's faster" and as witnessed by the existence of 2.0-compatible code even in npm @ https://github.com/npm/npm/pull/8163 this is probably not a tiny breakage to code in npm (I've probably done this myself). By shipping this feature we're breaking the "npm compatible platform" claim (ironically literally until https://github.com/npm/npm/pull/8163 is fixed!).

We have to have some respect for cowpaths that already exist and I'm anticipating no small amount of pain coming from this change.

petkaantonov commented 9 years ago

Using delete probably makes more sense than "assign to false-y" when the aim is to remove a given property from consideration.

it's not "assign to falsey" but assigning to null or undefined. Deleting properties is a JavaScript idiosyncracy, in any other language (dynamic or not) you would have object's properties be assigned to whatever is the null value in that language.

silverwind commented 9 years ago

The issue with the configurable getters/setters is that once you delete them, reassignment will make the property a plain property, possibly causing undefined behaviour. I've been looking at using Object.observe again to detect such deletions, but it just seems not possible.

@petkaantonov Do you have an estimate how the perf would be when using plain properties again? These could theoretically be observed to avoid unecessary assignment (jsperf here).

petkaantonov commented 9 years ago

@silverwind the properties are on the prototype, therefore delete on url objects has no effect regardless of their configurability

Edit: see http://www.ecma-international.org/ecma-262/5.1/#sec-8.12.7

domenic commented 9 years ago

I'll try again to make the point that this behaviour is exactly the behaviour of every DOM object ever, and should not be surprising to any JS developer.

chrisdickinson commented 9 years ago

I'm fretting about this a bit. Code has been written that relies on delete removing data from the url object, particularly before it's sent off to some other source. Some of this data may be sensitive, like delete url.parse(<string>).auth, as @rvagg points out. While I'm in favor of eventually defaulting to the new URL parser, non-deletable-properties-and-all, not warning (read: "exploding violently") on potentially leaked or missent info is chilling. Given that there's a tendency to parse strings into url objects, manipulate them, then hand them to APIs that perform requests in userland code, I agree with @rvagg that this is going to cause a lot of pain.

Given this, my preference is to ship the new URL code behind a feature flag for the duration of v2.0.0, which should be relatively short, given the impending V8 upgrade. If we can't get the code behind a feature flag in time for the v2.0.0 release, we should drop this from the release and get it into next for v3.0.0. Either way, the goal is to buy more time for this feature to address userland breakage, but not at the expense of this release.

silverwind commented 9 years ago

Deferring to 3.0.0 won't make it any better as we have no meaningful way to reach users, except through the changelog. I'd say put it in the list of breaking changes and be done with it. URL-based auth can't be that widespread anymore.

bnoordhuis commented 9 years ago

I agree with @silverwind. We're going to have to bite the bullet sooner or later if we want to ship this feature. I'm not sure if a feature flag would help or if it should default to on or off.

benjamingr commented 9 years ago

I'm pretty sure delete url.prop is in fairly high usage in userland

Any stats or data on that?

benjamingr commented 9 years ago

Here is some research, note this is mostly anecdotal but it should give us a broad idea, it's also partial:

These are the only actual results for delete url.path, out of these there is only one actual project:

For url.query:

Note all these delete a parameter off .query and not .query itself.

Samples for url.auth:

Samples for url.href:

Samples for url.host:

No one deletes .slashes or .protocol.

For anyone who is not used to GH code search results - this is very few matches/uses. Far less for example than the unhanldedRejection hook that was added only in io 1.4

Fishrock123 commented 9 years ago

Maybe @chrisdickinson can get some stats on it? I'm not against the change but it is pretty short notice, if that matters?

evanlucas commented 9 years ago

No one deletes .slashes or .protocol.

@benjamingr looks like npm actually deletes .protocol.

https://github.com/npm/npm/blob/master/lib/config/nerf-dart.js#L16

benjamingr commented 9 years ago

@evanlucas yes like I said that is very partial data - only checking GH code search and cases where the parsed URL is called URL.

silverwind commented 9 years ago

It's unlikely the variable would be called url as that's usually the module itself. Maybe uri or parsed will give better results.

silverwind commented 9 years ago

Quick search for auth deletion using above variable names:

chrisdickinson commented 9 years ago

as we have no meaningful way to reach users

We do! We have GitHub and pull requests. Delaying 'till 3.0.0 buys us time to make those PRs and get the ecosystem in a state where breakage is minimized, or at least the appropriate maintainers are made aware of the change.

URL-based auth can't be that widespread anymore.

Sure, but what about oauth tokens in GET params, for example?

chrisdickinson commented 9 years ago

Anec-data-ly: my local npm cache only has two repos that delete props from url.parse results: npm and relateurl. This may not be exhaustive: my delete handling isn't quite what I'd like it to be, TBH – I hadn't focussed on it until this came up!

silverwind commented 9 years ago

You could theoretically move to plain properties and print a deprecation warning through the delete event on Object.observe. But that would likely cost tons of perf.

Fishrock123 commented 9 years ago

Would it be possible to use Object.observe to correct the situation if delete was used? i.e. update properly, show deprecation and re-define the property?

silverwind commented 9 years ago

Fishrock: Nope, observe events don't seem to fire on getters/setters, only plain properties.

domenic commented 9 years ago

We shouldn't be using Object.observe anyway. It's a non-standard API and as such may be removed in future V8 versions.

silverwind commented 9 years ago

Or rather, they do seem to fire:

> o = {}; Object.defineProperty(o, "a", { get: function(){return 1}, configurable:true})
> Object.observe(o,console.log)
> delete o.a
[ { type: 'delete', object: {}, name: 'a' } ]

But something about the implementation did not work out last time I tried, probably because the accessors are on the prototype.

othiym23 commented 9 years ago

Deleting properties is a JavaScript idiosyncracy, in any other language (dynamic or not) you would have object's properties be assigned to whatever is the null value in that language.

False. I picked up the habit of using delete from Perl and Ruby, where it's the only way to remove both the key and the value from an associative array (which, to be clear, is also true in JavaScript, it's just that until ES2015, the only associative arrays built into JS are objects, which everyone uses interchangeably). What is an idiosyncrasy is the consequences for the JIT, because of how hidden classes work.

this behaviour is exactly the behaviour of every DOM object ever, and should not be surprising to any JS developer.

It may not be surprising if you're a browser developer. It is surprising to me as a back-end developer, because this is a change from behavior that's worked since the beginning of Node.

My point: I have an issue with some of the language in this thread. Improving the performance of url.parse is a worthwhile goal and this is a good change, but don't soft-pedal the significance of the change. Also, implying that only bad developers use a core idiom of the language is unhelpful.

As it stands, the project has a few choices:

  1. wait while I land @silverwind's PR (npm/npm#8163) and then roll out a new release for testing. The fact that we didn't notice this problem until now indicates that there's a hole in npm's tests, and that leads me to want to take more deliberation around getting this change hammered on for a little while, and extremely hesitant to roll it out as a hotfix.
  2. follow a path similar to the one suggested by @chrisdickinson and put it behind a flag
  3. ship iojs@2.0.0 with a broken npm

I don't really think 3 is a valid option, as it will break npm for all users of private registries, and possibly lead to the creation of broken npm configuration files for those users.

silverwind commented 9 years ago

@domenic isn't it on track for ES7?

@Fishrock123 here's the non-working case with the prototype getter (paste into repl, no 'delete' event):

function O () {}
Object.defineProperty(O.prototype, "a", { get: function(){}, configurable:true})
o = new O()
Object.observe(o,console.log)
delete o.a
monsanto commented 9 years ago

I think we should ship the new url module behind a flag, and move the getters from the prototype to the instance so we can warn on deletes. This gives devs the opportunity to see if their applications would silently fail with the new module. When we release for real we can move the getters back to the prototype.

As for "this is a breaking release, so it is OK" arguments: it's one thing to intentionally break something, it's another to find something broke unexpectedly at the absolute last minute. And to make things worse, this is a silent failure, and it even affects code that we ship with! And io.js already has a (undeserved?) reputation for being unstable compared to Node. And we are spending time arguing about this stuff when we should have shipped by now.

Is the new url implementation holding back progress in other ways? Is it so critical that it be in this release? I don't think we have anything to lose by backing out and putting a debug version behind a flag.

domenic commented 9 years ago

@silverwind I very much doubt it will get two implementations in shipping browsers in time for the ES2016 deadline. It should be considered as non-standard as any other future ES proposal.

We shouldn't try flags; we don't have the infrastructure, and building it will waste even more time. (What are we at now, 2 weeks until next V8 release/3.0.0?) If we're not going to make this breaking change in this release, then we need to roll back and do it in 3.0.0.

There's no way to warn on delete without a Proxy, which isn't in V8 yet.

monsanto commented 9 years ago

@domenic Throwing an exception on delete in strict mode is one type of warning, and @silverwind just showed that you can use Object.observe to warn for properties not on the prototype. Whether or not Object.observe is non-standard is irrelevant since this would just be for debugging purposes.

mikeal commented 9 years ago

How much work is it to put this behind a flag as opposed to just backing it out?

silverwind commented 9 years ago

Just throwing this out there: If we can detect the delete, we can just assign null and be done. No userside change required.

benjamingr commented 9 years ago

@silverwind we can detect the delete with a proxy but those are considered unstable and under a flag atm, and are also not really ES6 compliant. A proxy has a hook for this deleteProperty. This isn't really practical (like the Object.observe) part because of the performance overhead.

I honestly think that making users just change delete url.foo to url.foo = null isn't too bad. This is a major version update after all.

mikeal commented 9 years ago

We can detect a delete with Object.observe(), or at least it would appear so reading this http://www.html5rocks.com/en/tutorials/es7/observe/

We should just set it to null when observing deletes.

We should not require user intervention or break npm.

domenic commented 9 years ago

Object.observe is nonstandard and slows down any object it touches. If we introduce that we might as well just roll back since the whole point of this change was performance.

monsanto commented 9 years ago

I feel like there is a lot of crosstalk here. Using Object.observe requires putting the getters on the instance + observing every instance which would defeat the purpose of a superfast implementation. It would only be of use on a debug version behind a flag. Setting null automatically is a non-starter.

othiym23 commented 9 years ago

npm/npm#8163 does not pass npm's full test suite, even on iojs@1.8.1. I'm going to keep poking at this, but it's Sunday and I'm probably not going to spend more than another hour or two poking at this on my day off.

Unfortunately, this is not a trivial update for npm, and it's going to take some time to get this fixed and to make sure that whatever the full solution does doesn't break npm under older versions of Node.

mikeal commented 9 years ago

@domenic because we'd have at least a full release cycle using the rest of this new code and testing it in the wild before asking users to change behavior in order to improve the performance.

chrisdickinson commented 9 years ago

Notably, Object.observe is asynchronous, so beyond the perf concerns, I don't think it'd actually fix the problem.

I think at this point we should back out the change and introduce it behind a flag on the v2.x line, making it a candidate for v3 or v4. I agree with @domenic that we shouldn't block the v2.0.0 release for this to land with a feature flag.

mikeal commented 9 years ago

I think it would be better to back it out then. We'll want to re-evaluate if this is worth landing being that there is no way to fix this behavior without using proxies and we still don't know when those will land and what perf impact they'll have when they do. The assumption we were under when we agreed to take the change was that the only breaking change in clone() behavior, I'm not sure the TC would find this level of breakage acceptable or not so it's better just to put it back through the process.