Fun with Ruby: Returning in blocks "overwrites" outside return values

Updated . Posted . Visible to the public.

In a nutshell: return statements inside blocks cause a method's return value to change. This is by design (and probably not even new to you, see below) -- but can be a problem, for example for the capture method of Rails.


Consider these methods:

def stuff
  puts 'yielding...'
  yield
  puts 'yielded.'
  true
end

We can call our stuff method with a block to yield. It works like this:

>> stuff { puts 'hi!' }
yielding...
hi!
yielded.
=> true

So the block is yielded and our result is true. Just like you would expect.

Now, consider this method:

def proxy_thing
  stuff do
    return 42
  end
end

Looks quite similar, right? Check this:

>> proxy_thing
yielding...
=> 42

Woah.

What is happening here?

  • We no longer get the true return value from our stuff method, but the one from the block which was sent to it.
  • Also, any code after the block is being yielded is no longer executed.

Both are caused by the return inside of the block.

This behavior is by design, just so that you can break out of a block -- and you have probably seen it before, e.g. a return in an each block.

Impact on Rails applications

Since this is something that you would have to do yourself, you could just not do it.

But what if code uses return in some cases and doesn't return in others? One example is Rails' capture:

def capture(*args, &block)
  # Return captured buffer in erb.
  if block_called_from_erb?(block)
    with_output_buffer { block.call(*args) }
  else
    # Return block result otherwise, but protect buffer also.
    with_output_buffer { return block.call(*args) }
  end
end

This can come bite you with extremely unexpected behavior of your application, as this will mean a slightly different result of your capture:
For blocks that were called_from_erb?, you will (as in the example above) get the method's return value, which in the case of capture is the buffer that you concat on. When using rails_xss that would be a SafeBuffer -- but if you end up in the else case, your capture will receive the String result from the block instead of the buffer. All html_safe? information on it is lost!

A possible fix for capture

Admittedly, you'll rarely end up in such cases, but if you do, this is the way out I took:

module CaptureHelper

  def capture_as_erb(&block)
    # Ensure that the block's `concat` output is captured, not the block's return value.
    # This is necessary for blocks that are not actually ERB blocks, since they would return their
    # `String` result, even if they did concat to a `SafeBuffer`.
    __in_erb_template = true
    capture do
      block.call
    end
  end

end

Then, just use capture_as_erb instead of capture.

Arne Hartherz
Last edit
Emanuel
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2012-09-06 13:25)