Posted almost 7 years ago. Visible to the public.

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

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:

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

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

Copy
>> 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:

Copy
def proxy_thing stuff do return 42 end end

Looks quite similar, right? Check this:

Copy
>> 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:

Copy
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:

Copy
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.

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Owner of this card:

Avatar
Arne Hartherz
Last edit:
2 months ago
by Besprechungs-PC
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Arne Hartherz to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more