Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
#method_missing makes me eat my words

A while back I wrote about private methods in ActiveRecord objects, and how Rails 2.2 makes them behave as they should. ActiveRecord associations will no longer respond to private methods defined on their targets; however, my colleague Joseph pointed out that they also no longer respond to methods defined via #method_missing on their targets. Which sucks horse poop through a straw, to some extent.

In my original post I wrote this, fully aware that I was writing lies, but mostly in a rush to get on to my point at the time:

Sometimes I’m forced to use #send:

method_name = extract_method_name_from_the_aether
some_object.send(method_name)

In order to make this code correct with regard to access control I have to add cruft:

method_name = extract_method_name_from_the_aether
some_object.send(method_name) if some_object.respond_to?(method_name)

The astute reader will note that the second code block doesn’t actually approximate the behavior of calling a private method via function call syntax. It should look more like this:

method_name = extract_method_name_from_the_aether
raise NoMethodError if some_object.private_methods.include?(method_name)
some_object.send(method_name)

Two things to notice here: first, the original code didn’t throw an exception if it failed to call the method; second, and much more importantly, the set of private methods is not the complement of the set of methods an object will respond to. So, using !#respond_to? as the condition for preventing the method call is too restrictive. The fact that this code will prevent calling a non-private method defined via #method_missing clearly shows this.

Now, I know the Rails core team chose to use #respond_to? in this particular case for a good reason (which my original patch did not take into consideration): performance. Some simple investigation quickly shows that #include? is an order of magnitude slower than #respond_to? Method calls happen quite a lot, so slow is bad.

I’ve submitted another patch that should correct the #method_missing behavior without dragging everything to a halt. Building on my previous examples, the code looks something like this:

method_name = extract_method_name_from_the_aether
raise NoMethodError if !some_object.respond_to?(method_name) && some_object.private_methods.include?(method_name)
some_object.send(method_name)

Note that the conditional expression is now redundant; the first condition cannot be false if the second condition is true. However, an object will likely respond to the majority of method calls sent to it. The first conditions will (quickly) evaluate to false in these cases, short-circuiting the conditional. In the minority of cases where the first condition evaluates to true, the second condition will (slowly, but correctly) determine if the method call violates access control.

Unfortunately, sensible as it may seem, this change actually breaks has_one :through associations, because of collection-specific functionality they inherit from has_many :through associations. (Oh yes, has_one :through associations are collections, based on their place in the ActiveRecord inheritance hierarchy. Or, they were; read more about that here).

And, finally, I think it’s important to note that none of these machinations in Rails would be necessary if #send (and #send!) actually worked as it should. Heads up, Matz.

Comments
  1. hennk says:

    Wouldn’t it be easier to just implement respond_to? on the target, too, using the same logic you use in method_missing to determine if you want to handle the message?

    I did something similar while updating our project to Rails 2.2, although it was “only” a simple has_one association.

  2. Adam Milligan says:

    Are you suggesting reimplementing #respond_to? on whatever arbitrary class each association references? That would mean changing the behavior of #respond_to? for all instances of those classes. Suddenly the behavior of #respond_to? would be inconsistent from class to class, depending on whether you reference a particular class in an association or not.

    What’s the upside of this approach? Or have I misunderstood your suggestion?

  3. Adam Milligan says:

    @hennk,

    Upon reflection I believe I misunderstood. You’re suggesting that the author of the class that defines a method via #method_missing also change #respond_to?. I originally read the comment to say that the association proxy should modify #respond_to? on the target class, which seemed to me like a bad idea.

    Now that I’m clear, this makes sense. It would certainly make the target class more correct, with regard to interrogating it about its capabilities.

  4. hennk says:

    Right, what we did when migrating to Rails 2.2 was to additionally implement respond_to? in the target, where for Rails 2.0 we only implemented method_missing.

    If done this way I think it won’t break with 2.2 changes, needs no patches to Rails and seems more correct, like you already wrote.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *