Routing parameters with a dot

Posted by Andreas on Tuesday, May 05, 2009 at 07:35 (CEST)

Last week I received an interesting bug report for an application I’m working on at the office. It has a controller with an index action that displays a list of items which can be filtered by tags. A tester reported that every time he chooses to filter the list by a tag that contains a dot, the site returns an error. First this seems strange since tags with a dot worked perfectly fine in tests.

But after digging deeper, I found a surprising reason for this strange error: To make URLs look nicer, I defined an extra route like this:

1 map.things 'things/:tag', :controller => 'things', :action => 'index',
2   :tag => nil

The intention was, to have /things/foo instead of /things&tag=foo as the URL. This actually worked fine – except for cases when the tag contained a dot.

Rails seems to magically append :format to every rule so that different result formats are possible with one rule (like /things.html and /things.json). If a tag contained a dot, the URL requested was /items/foo.bar, which Rails parsed into :tag => 'foo', :format => 'bar'.

So the controller’s index action was requested to display all entries for tag foo and output results in format bar – which is unknown and unsupported and therefore resulted in an error page.

This behaviour seems to be hardcoded into Rails and I couldn’t find a way to disable it. But luckily a workaround is pretty easy. The trick is to make the tag parameter’s regular expression greedy, so that it covers the format part as well. Using the :requirements option to a route, we can set the regexp for a parameter:

1 map.things 'things/:tag', :controller => 'things', :action => 'index',
2   :tag => nil, :requirements => { :tag => /.+/ }

Now it works as intended. :format is always ignored now and the whole string is put into params[:tag].

6 comments

Gravatar
Piotr Sarnacki wrote about 9 hours later:

I would rather do: :tag => %r([^/;,?]+)

Gravatar
Marcos Arias wrote about 19 hours later:

I think the default behavior of dots in routing is correct and should not be changed. When I write “http://example.com/tags/foo.js”, I expect to get a javascript file, not a tag name.

IMHO a better solution to this is to use ActiveSupport::Inflector.parameterize and save a sanitized version of the tag name without dots and other non desirable characters, then using it in the url. A tag could have a “name” field and a “url” field, for expample.

A tag “foo.bar;baz” will result in “foo-bar-baz” url tag using parameterize method

A very very simplistic approach (sorry for the lack of indentation):

class Tag < AR
def after_save
url = name.parameterize
end
end

class ArticleController < AC
def articles_by_tag
@tag = Tag.find_by_url(params[:tag_url])
@articles = @tag.articles
end
end

- Routing
map.articles_by_tag ‘articles/tag/:tag_url’, :action => ‘articles_by_tag’, :controller => ‘articles’

And that’s it, no magic regexp in the route. I hope you find this kinda useful.

Bye!

Gravatar
Andreas wrote 1 day later:

@Piotr: Good idea. I’m not sure if my /.+/ would still with additional parameters, but your solution certainly would.

@Marcos: Of course you usually don’t want to change the default behaviour, since the format param is pretty useful. It was just this case where it got into my way. To parameterize the tag name would mean that it wouldn’t be unique anymore (foo.bar and foo-bar wouldn’t be the same) – ok, maybe that’s just a minor gotcha.

URL-Parameters with special chars like ? and & are properly escaped. I was thinking about simply manually escaping dots additionally like things_url(:tag => some_tags.gsub('.', '%2E')), which I guess should work also (untested), since %2E should be resolved to a dot when Rails unescapes the request parameter.

This could also be done automatically (since it shouldn’t harm anything else) by aliasing the url_for helper. Maybe it wouldn’t be so bad if Rails would do this natively – but then after all, it’s just a very special case.

Gravatar
Vítor Baptista wrote 3 months later:

Thanks for your post. It helped me a lot, but I needed something more. I wanted to accept parameters with a dot while still accepting formats. For example, I want /project/ror/2.3.3 AND /project/ror/2.3.3.xml to work. This proved to be somewhat difficult.

At first, I searched if the params[:id] =~ /\.xml$/ were true. If it were, set the params[:format]. But found a better way. It is far from perfect, but works…
http://github.com/vitorbaptista/caboclo/blob/43d34f1c0988cf9aeb364465a9f7e40e62f8b063/www/config/routes.rb

It isn’t DRY at all. I also need to guarantee that there are no versions whose names ends with any of the supported formats. And, if I add another format (e.g. .yaml) and there’s already one version name ending with it, I have a problem.

Do you know a better way?

Thanks.

Gravatar
Vítor Baptista wrote 3 months later:

Found a sightly better way. Still far fom perfect, but getting closer…

http://github.com/vitorbaptista/caboclo/blob/79629aa758f86a6bca08b8d54b871b2e56e390ac/www/config/routes.rb

Gravatar
Andreas wrote 3 months later:

Although it gets a bit tricky, you can do that also with a fancy requirement regexp:

1 # /things/foo         => :tag = 'foo'         :format = nil
2 # /things/foo.bar     => :tag = 'foo.bar'     :format = nil
3 # /things/foo.bar.xml => :tag = 'foo.bar'     :format = 'xml'
4 # /things/foo.bar.baz => :tag = 'foo.bar.baz' :format = nil
5 map.things 'things/:tag.:format', :controller => 'things',
6   :action => 'index', :tag => nil,
7   :requirements => { :tag => /.+(?=\.(html|xml|js))|.+/ }

Leave a comment

(required)

(required; will not be published)

(optional)

(required; Textile formatting allowed)