Using Action Controller Renderers in Rails 5 with Devise

Rails 5 offers a new option for template rendering, allowing us to render views outside of controller actions. This feature will make it easy to render views from background tasks.

This excellent and very clear blog post from by Rails contributor Ravil Bayramgalin shows how to use ApplicationController.renderer. Check it out for a comprehensive look.

This post will provide a quick look at using the renderer in a Rails 5 app that uses Devise.

Background

In a Rails 5 app I'm currently working on, I attempted to use ApplicationController.renderer to render a template that is then converted into Markdown, in order to be written to REAMDE.md files and posted to GitHub repos using the GitHub API. However, I ran in to some trouble using the renderer in an application that is using Devise.

The Renderer and the Request Environment

Under normal circumstances, the following code will work to render a given template outside of a controller action:

renderer = ApplicationController.renderer.new  
renderer.render(file: 'posts/show.html.erb', assigns: {post: @post })  

Let's say we're rendering a template for a blog post's show page and that the template looks something like this:

< h1 > <%= @post.title %> < /h1 >  
< p > <%= @post.content %> < /p >  

We can, with the code above, instantiate our renderer and use the #render instance method, even assigning the instance variables used in the template.

Gotcha Alert: However, some gems, including Devise, include helper methods that rely on environment data from various middlewares. So, the above code, in a Rails 5 app that is using Devise, will through the following error:

ActionView::Template::Error: undefined method `authenticate' for nil:NilClass  

Oh no! Luckily for us, though, we can grab the missing environment data and manually supply it to the internal environment of our renderer.

Hacking Around the Problem

Our undefined method authenticate error is being thrown because the renderer's environment is missing the warden key, which points to the Warden::Proxy object.

We can access it in our request environment like this:

env["warden"]  
# => Warden::Proxy:70229737086020 @config={:default_scope=>:user, :scope_defaults=>{}, :default_strategies=>{:user=>[:rememberable, :database_authenticatable]}, :intercept_401=>false, :failure_app=>#<Devise::Delegator:0x007fbf4054df88>}

We can add it to our renderer's environment, along with some other info we need, like this:

renderer.instance_variable_set(:@env, {"HTTP_HOST"=>"localhost:3000",  
  "HTTPS"=>"off",   
  "REQUEST_METHOD"=>"GET",   
  "SCRIPT_NAME"=>"",   
  "warden" => warden})

Then, we can use our renderer:

renderer.render(file: 'posts/show.html.erb', assigns: {post: @post })  

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus