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