Ticket #118 (closed defect: fixed)

Opened 1 year ago

Last modified 1 year ago

PATCH render(:partial ) eats rest of view

Reported by: jon.egil.stra..@gmail.com Assigned to:
Priority: critical Milestone: 0.3.x
Component: Merb Keywords:
Cc:

Description (Last modified by rogelio.samo..@gmail.com)

When using render(:partial => 'form) in a view, all output before the render is lost.

Here's a recipe to reproduce (thanks to Brian Candler):

  merb -g renderbug

==> dist/app/controllers/foo.rb <==
class Foo < Application
  def bar
    render :layout => :none
  end
end


==> dist/app/views/foo/bar.erb <==
<div>This bit comes first</div>
<%= render :partial => ‘foo_form’ %>
<div>This bit comes at the end</div>


==> dist/app/views/foo/_foo_form.erb <==
<div>This bit goes in the middle</div>

Edit dist/conf/merb.yml to comment out SQL sessions and uncomment memory sessions, then fire up merb and point a browser at http://localhost:4000/foo/bar

The text “This bit comes first” is missing.

Attachments

ruby-debug of merb view.txt (20.1 kB) - added by jon.egil.stra..@gmail.com on 08/09/07 14:24:38.
A typical debug session with a debug'ed view.
erubis-debug.diff (0.7 kB) - added by b.candl..@pobox.com on 08/10/07 00:48:10.
Patch to erubis to demonstrate the problem with _buf scoping
merb-renderbug.diff (2.3 kB) - added by b.candl..@pobox.com on 08/15/07 14:01:21.
Patch which may fix the problem
merb-renderbug-trunk.diff (2.2 kB) - added by b.candl..@pobox.com on 08/18/07 05:29:13.
Corresponding patch against trunk

Change History

08/09/07 13:13:21 changed by jon.egil.stra..@gmail.com

Confirmed on

  • ubuntu 6.06 / ruby 1.8.4 (2005-12-24) [i486-linux]
  • ubuntu 7.04 / ruby 1.8.5 (2006-08-25) [i486-linux]

08/09/07 13:51:29 changed by rogelio.samo..@gmail.com

  • description changed.

(in reply to: ↑ description ) 08/09/07 13:55:22 changed by rogelio.samo..@gmail.com

It seems like this bug is Debian-flavored distro-specific. In addition, to the ones stated above here are mine:

  • ubuntu 6.06 / ruby 1.8.4 (2005-12-24) [i486-linux]

* - ubuntu 6.10 / ruby 1.8.6 (2006-08-25) [i486-linux]

  • ubuntu 7.04 / ruby 1.8.5 (2006-08-25) [i486-linux]

* - Debian 4.0 / ruby 1.8.5 (2006-08-25) [i486-linux]

Replying to jon.egil.stra..@gmail.com:

When using render(:partial => 'form) in a view, all output before the render is lost. Here's a recipe to reproduce (thanks to Brian Candler): {{{ merb -g renderbug }}} {{{ ==> dist/app/controllers/foo.rb <== class Foo < Application def bar render :layout => :none end end ==> dist/app/views/foo/bar.erb <== <div>This bit comes first</div> <%= render :partial => ‘foo_form’ %> <div>This bit comes at the end</div> ==> dist/app/views/foo/_foo_form.erb <== <div>This bit goes in the middle</div> }}} Edit dist/conf/merb.yml to comment out SQL sessions and uncomment memory sessions, then fire up merb and point a browser at http://localhost:4000/foo/bar The text “This bit comes first” is missing.

08/09/07 14:00:39 changed by jon.egil.stra..@gmail.com

08/09/07 14:22:46 changed by jon.egil.stra..@gmail.com

This is how you add ruby-debug to your view: http://pastie.caboo.se/86409

Then just start up merb as normal, go to the view and your merb console will now be a ruby-debug console.

08/09/07 14:24:38 changed by jon.egil.stra..@gmail.com

  • attachment ruby-debug of merb view.txt added.

A typical debug session with a debug'ed view.

08/09/07 14:31:22 changed by jon.egil.stra..@gmail.com

and the code for that step-through is http://pastie.caboo.se/86342

08/10/07 00:41:46 changed by b.candl..@pobox.com

I have also replicated the fault using CentOS 4.5, running the ruby-1.8.5 RPM package from http://dev.centos.org/centos/4/testing/i386/RPMS/

$ uname -a
Linux localhost.localdomain 2.6.9-55.0.2.plus.c4 #1 Fri Jul 6 05:04:29 EDT 2007 i686 i686 i386 GNU/Linux
$ ruby -v
ruby 1.8.5 (2006-08-25) [i386-linux]

08/10/07 00:48:10 changed by b.candl..@pobox.com

  • attachment erubis-debug.diff added.

Patch to erubis to demonstrate the problem with _buf scoping

08/10/07 01:03:28 changed by b.candl..@pobox.com

The following demonstrates what I believe to be the underlying source of the problem.

Apply erubis-debug.diff to your erubis installation; this adds some puts statements showing the value and object_id of _buf at the start of the template, when it is initialised, and at the end.

When you fire up merb, this is what you see:

You need to install the mailfactory gem to use Merb::Mailer
you must install the markaby gem to use .mab templates
you must install the builder gem to use .rxml templates
you must install the haml gem to use .haml templates
memory session mixed in
Compiling routes..
merb init called
Connecting to database...
_buf already exists! "---\n# Hostname or IP address to bind to. \n:host: 127.0.0.1\n\n# Port merb runs on or starting port for merb cluster.\n:port: \"4000\"\n\n# In development mode your controller classes get reloaded on every request,\n# and templates are parsed each time and not cached. In production mode\n# templates are cached, as well as all your classes\n:environment: development\n\n# Uncomment for memory sessions. This only works when you are running 1 merb\n# at a time. And sessions do not persist between restarts.\n:memory_session: true\n\n# Turn on memcached sessions.\n# Requires these lines in merb_init.rb (and a running memcached server):\n#   require 'memcache_util'\n#   CACHE = MemCache.new('127.0.0.1:11211', { :namespace => 'my_app' })\n# :mem_cache_session: true\n\n# This turns on the ActiveRecord sessions with rails parasite mode if\n# active_support gem is installed. Skeleton app comes with a migration to\n# create the sessions table. Or you can point merb to  the same sessions\n# table that your rails app uses to share sessions between merb and rails.\n# :sql_session: true\n:log_level: debug\n\n# Uncomment to use the merb upload progress\n#:config: dist/conf/upload.conf\n\n# Uncomment to cache templates in dev mode. Templates are cached\n# automatically in production mode.\n#:cache_templates: true\n\n# Uncomment and set this if you want to run a drb server for upload progress\n# or other drb services.\n#:drb_server_port: 32323\n\n# If you want to protect some or all of your app with  HTTP basic auth then\n# uncomment the following and fill in your credentials you want it to use.\n# You will then need to set a 'before' filter in a controller.  For example:\n#   before :basic_authentication\n#:basic_auth: \n#  :username: ezra\n#  :password: test\n#  :domain: localhost\n\n# Uncomment this if you want merb to daemonize when you start it. You can also\n# just use merb -d for the same effect. Don't uncomment this if you use the\n# cluster option.\n#:daemonize: true\n\n# Uncomment this to set the number of members in your merb cluster. Don't set\n# this and :daemonize: at the same time.\n#:cluster: 3\n" (-605254948)
_buf initialised to "" (-613461758)
_buf final value "development:\n  adapter: mysql\n  database: sample_development\n  username: teh_user\n  password: secrets\n  host: localhost\n  socket: /tmp/mysql.sock\n\ntest:\n  adapter: mysql\n  database: sample_test\n  username: teh_user\n  password: secrets\n  host: localhost\n  socket: /tmp/mysql.sock\n\nproduction:\n  adapter: mysql\n  database: sample_production\n  username: teh_user\n password: secrets\n  host: /tmp/mysql.sock\n  \n" (-613461758)
Loaded DEVELOPMENT Environment...

That's strange, but more importantly, when you hit it with the web request for /foo/bar, you get the following:

_buf already exists! "development:\n  adapter: mysql\n  database: sample_development\n  username: teh_user\n  password: secrets\n  host: localhost\n  socket: /tmp/mysql.sock\n\ntest:\n  adapter: mysql\n  database: sample_test\n  username: teh_user\n  password: secrets\n  host: localhost\n  socket: /tmp/mysql.sock\n\nproduction:\n  adapter: mysql\n  database: sample_production\n  username: teh_user\n  password: secrets\n  host: /tmp/mysql.sock\n  \n" (-613461758)
_buf initialised to "" (-613500338)
_buf already exists! "<div>This bit comes first</div>\n" (-613500338)
_buf initialised to "" (-613505918)
_buf final value "<div>This bit goes in the middle</div>\n" (-613505918)
_buf final value "<div>This bit goes in the middle</div>\n\n<div>This bit comes at the end</div>\n" (-613505918)

You can see that in the 'outer' call to erubis, _buf was initialised to oid -613500338. But by the time the 'outer' call ends, _buf is oid -613505918, which is the object which was created in the 'inner' call to erubis. In other words, it seems that _buf is the same local variable in both inner and outer scopes.

NOW: note that if you write a small test program to invoke the controller render action directly, rather than running under mongrel, everything works properly:

[candle..@localhost renderbug]$ cat test.rb
require 'rubygems'
require 'merb'

class Foo < Merb::Controller
  def params
    {}
  end
  def test
    render(:template=>"foo/bar", :layout=>:none)
  end
end

p Foo.new.test
[candle..@localhost renderbug]$ ruby test.rb
You need to install the mailfactory gem to use Merb::Mailer
you must install the markaby gem to use .mab templates
you must install the builder gem to use .rxml templates
you must install the haml gem to use .haml templates
_buf unset
_buf initialised to "" (-606331628)
_buf unset
_buf initialised to "" (-606333898)
_buf final value "<div>This bit goes in the middle</div>\n" (-606333898)
_buf final value "<div>This bit comes first</div>\n<div>This bit goes in the middle</div>\n\n<div>This bit comes at the end</div>\n" (-606331628)
"<div>This bit comes first</div>\n<div>This bit goes in the middle</div>\n\n<div>This bit comes at the end</div>\n"

So the problem is triggered either by running under mongrel, or by using more of the request parsing functionality than test.rb is using.

08/10/07 02:26:43 changed by b.candl..@pobox.com

I can also get the same effect without mongrel by using merb -i, although I'm not sure this is the correct way to invoke a controller by hand:

[candle..@localhost renderbug]$ merb -i
... snip ...
Loaded DEVELOPMENT Environment...
irb(main):001:0> c = Foo.new
=> #<Foo:0xb7114a24 @_benchmarks={}>
irb(main):002:0> c.instance_eval { @_params = {:action=>:bar} }
=> {:action=>:bar}
irb(main):003:0> c.bar
_buf already exists! "development:\n  adapter: mysql\n  database: sample_development\n  username: teh_user\n  password: secrets\n  host: localhost\n  socket: /tmp/mysql.sock\n\ntest:\n  adapter: mysql\n  database: sample_test\n  username: teh_user\n  password: secrets\n  host: localhost\n  socket: /tmp/mysql.sock\n\nproduction:\n  adapter: mysql\n  database: sample_production\n  username: teh_user\n  password: secrets\n  host: /tmp/mysql.sock\n  \n" (-613351448)
_buf initialised to "" (-611841138)
_buf already exists! "<div>This bit comes first</div>\n" (-611841138)
_buf initialised to "" (-611848358)
_buf final value "<div>This bit goes in the middle</div>\n" (-611848358)
_buf final value "<div>This bit goes in the middle</div>\n\n<div>This bit comes at the end</div>\n" (-611848358)
=> "<div>This bit goes in the middle</div>\n\n<div>This bit comes at the end</div>\n"

(follow-up: ↓ 12 ) 08/15/07 14:00:37 changed by b.candl..@pobox.com

I have learned a bit more about the problem. As far as I can see:

  • Merb expands its configuration files at startup using Erubis. It does this by calling #result
  • render later expands its Erubis templates by calling #evaluate

Now, compare these two Erubis methods (in erubis-2.4.0/lib/erubis/evaluator.rb):

    ## eva..@src) with binding object
    def result(_binding_or_hash=TOPLEVEL_BINDING)
      _arg = _binding_or_hash
      if _arg.is_a?(Hash)
        ## load _context data as local variables by eval
        #eval _arg.keys.inject("") { |s, k| s << "#{k.to_s} = _arg[#{k.inspect}];" }
        eval _arg.collect{|k,v| "#{k} = _arg[#{k.inspect}]; "}.join
        _arg = binding()
      end
      return eva..@src, _arg, .@filename || '(erubis)'))
    end

    ## invoke context.instance_eva..@src)
    def evaluate(context=Context.new)
      context = Context.new(context) if context.is_a?(Hash)
      #return context.instance_eva..@src, @filename || '(erubis)')
      @_proc ||= eval("proc { ..@src} }", TOPLEVEL_BINDING, @filename || '(erubis)')
      return context.instance_eval(&am;..@_proc)
    end

Notice that #result simply evals the Ruby string, i.e. "_buf=""; ...; buf". This defines a local variable _buf, and if you don't pass in a binding, it's done at TOPLEVEL_BINDING.

#evaluate is better behaved; it calls "proc { _buf=""; ...; buf }". But this will bind to the same _buf as was defined before if _buf was defined at the top level. And this is what happens when merb runs its config files through Erubis.

I don't pretend to understand Ruby bindings properly, but the attached patch merb-renderbug.diff makes the problem go away for me at least, by calling #result(binding) instead of #result

08/15/07 14:01:21 changed by b.candl..@pobox.com

  • attachment merb-renderbug.diff added.

Patch which may fix the problem

08/15/07 14:04:15 changed by b.candl..@pobox.com

Note: after applying this patch, if you have an existing project you will also need to edit its dist/conf/merb_init.rb to change .result into .result(binding) when parsing database.yml:

...
conn_options = YAML::load(Erubis::Eruby.new(IO.read("#{DIST_ROOT}/conf/database.yml")).result(binding))
...

(in reply to: ↑ 10 ) 08/16/07 14:38:26 changed by pmisiowi..@mac.com

I've confirmed this was a problem on Mac OSX 10.4.10 with Ruby 1.8.6 and that the patch resolved the issue. Thanks!

08/17/07 12:00:33 changed by b.candl..@pobox.com

  • summary changed from render(:partial ) eats rest of view to PATCH render(:partial ) eats rest of view.

08/18/07 05:29:13 changed by b.candl..@pobox.com

  • attachment merb-renderbug-trunk.diff added.

Corresponding patch against trunk

08/21/07 05:44:58 changed by jon.egil.stra..@gmail.com

  • status changed from new to closed.
  • resolution set to fixed.

(In [420]) Patching renderbug, fixes #118, good work b.candler