Sunday, January 13, 2008

Ruby on Rails: How to set url_for defaults

I work as a Ruby on Rails programmer, so from time to time I'm going to post something about this language. Recently I implemented HTTPS into Rails application and had to figure out how to set defaults for url_for method: complete url (not only relative path) and http protocol. It's a little bit complicated, so here I am posting how I did that, it might save someone a couple of hours.

So, how do we set defaults for url_for method? First obvious solution would be to copy and paste this method from action controller into our application controller and add some code at the beginning of the method:

1
2
3
4
5
6
7
8
def url_for(options = nil) #:doc:

  # added url_for defaults
  options[:path_only] ||= false
  options[:protocol] ||= 'https'

  case options || {}
...

Of course that would be a lame solution, it's a bad idea to rewrite core methods (unless they are meant to be rewritten, for example the method I'll mention in next paragraph). If someone change this core code in future (which is quite possible), it might break the application.

Instead you can use default_url_options method to set url_for defaults. This method return a hash of url_forparameters, which is empty by default. So you can just write this into your application controller:

1
2
3
def default_url_options(options)
  { :only_path => false, :protocol => 'https' }
end

And we're done, right? No, that's not enough. It took me a while to figure out why certain URLs still don't work - URLs in views won't be affected by default_url_options, because they use url_for helper method, not the one from action controller. Yes, there are actually two url_for methods in Rails, one in controller, other in helper (url_helper.rb). And while you can rewrite defaults for the controller, but can't rewrite some defaults for helper - :only_path parameter is hardcoded to true, we want it to be false. What to do?

It's easier to fix than it seems. Just add this line into application controller:

1
helper_method :url_for

This causes that controller url_for method will be used also as helper method and it rewrites core url_for helper method, so default parameters will be applied on all URLs.

That's all, hopefully this will help someone.

10 comments:

Anonymous said...

This *almost* works for me, and thanks for the tip. My only problem is that while it works for my miscellaneous named routes, it doesn't work for any URL generated by map.resources or map.root.

So I have, for example, map.resources :people, but people_url doesn't get the https protocol. On the other hand I have map.responses 'responses/:program', :controller => 'programs', :action => 'responses', :program => 'forum' and responses_url(:program => 'quest') does get the https protocol.

Any ideas? Thanks!

Ben Kimball

Anonymous said...

Oh, er, and I'm on Rails 2.0.2.

lucastej said...

Hi Ben,

sorry for the late reply, here's the answer if you still want it:

For resources you gotta use :requirements parameter, for example:
map.resources :people, :requirements => { :protocol => 'https' }

You can find more details here:
http://siannopollo.blogspot.com/2007/08/rails-and-ssl-https.html

Unknown said...

Hey

Just wasted a few hours trying to work out my url_for woes before stumbling on this post.
Never thought to the check that two different url_for methods could be cause of my code sometimes being reached and sometimes not. doh!

Nice to know about the url_for_defaults method =)

Cheers
Dave Smylie

Dave M said...

You, sir, are my hero. I've been wondering how to do this for a while, but just too busy (and lazy) to read through the source to figure it out. I didn't even know default_url_options existed. I've needed an easy way to append a query param to every url for a share tracking product we use. This is perfect.

Thanks so much for sharing.

itinsley said...

really appreciate this post. Just what i need. i'm going to try it now.

Unknown said...

Still useful. Even in 2012 and Rails 3. - Thanks

jon said...

Thanks for the great tip! I did a variation:

def default_url_options(options = nil)
if ROUTES_PROTOCOL == 'https'
{ :only_path => false, :protocol => 'https' }
else
{ :only_path => false, :protocol => 'http' }
end
end

And set ROUTES_PROTOCOL in environment.rb (default to http) and production.rb (to https)

The only weird thing? I have to set config.cache_classes = false for it to work... !!

Looking into that now. Probably some sort of ruby class opening issue?

Noha said...

Thank you so much for your post. I know it's 4 years old but I'm hoping anyone would figure out why I'm getting this error:
This webpage has a redirect loop
The webpage at example.com has resulted in too many redirects

Anonymous said...

Thanks!

A great help, my solution then included a call to `super` as well for when my API_HOST var isn't set (e.g. in local dev)


class ApplicationController < ActionController::API

def default_url_options
super.merge(custom_url_options)
end

def custom_url_options
options = {}
options[:host] = ENV["API_HOST"] if ENV["API_HOST"].present?
options
end
end