Obscure Rails bug: respond_to format.any

I found an odd bug in Rails today. Odd, in the sense that it's not so much broken, as working in a way that's different than one would expect.

In a controller, one uses respond_to to present the appropriate response, as determined by the requested mimetype. If you want to respond to a whole clump of mimetypes in the same way, you might expect format.any to act as a catch-all for all the types you didn't already address.

So, for example, if we're writing an authentication system, we want web browsers redirected to the login page. But for non-browsers (eg, curl asking for XML, or Ajax asking for JSON, or some API asking for a vcard, whatever), it doesn't make any sense to direct them to a login page. We should ask them to use http basic authentication.

Here's the way this is handled by the current version of restful_authentication:

def access_denied
  respond_to do |format|
    format.html do
      store_location
      redirect_to new_<%= controller_singular_name %>_path
    end
    format.any do
      request_http_basic_authentication 'Web Password'
    end
  end
end

Except, this doesn't actually work. If you make a request for a non-html mimetype, you'll get a 406 ("that format doesn't work here anymore"), not a 401 ("unauthorized").

Presently, format.any actually looks something like this:

def any(*args, &block)
  args.each { |type| send(type, &block) }
end

And, if you dig around a little in the mime_responds tests, you'll see that you're supposed to call #any like so:

respond_to do |type|
  type.html { render :text => "HTML" }
  type.any(:js, :xml) { render :text => "Either JS or XML" }
end

Which, effectively, makes #any useless as a high-level catch-all for things like #access_denied. It's interesting that you can use it as a sort of multi-type, but that hardly seems the most common application. (Given the scarce documentation and meta-magic shenanigans in MimeType::Responders, I kinda doubt #any is used all that often. I only know about it because of restful_authentication.)

I've created a patch that changes #any's signature. It can now take 0 or any number of arguments. In the case where it has zero arguments, it assumes the mimetype requested (via the Rails-standard format parameter) or the first allowed mimetype (set by the request headers). In other words, it allows code like #access_denied, above from restful_authentication, to work as intended.

If you can, please take a moment to +1 the patch.

Permalink • Posted in: programming, ruby on rails, webComments (2)

Comments

Philip (flip) Kromer May 14, 2008

Please add 8 hours worth of my life onto the pile of dead minutes from this ... I was pulling my hair out.

Luckily I found your blog post as I was getting ready to waste another hour submitting a bug to the wrong group (rspec). Thank you !!!!

Sam Goldstein Nov 20, 2008

Nice catch!

I'd run into this same issue with restful_authentication, and found that changing format.any to format.xml got http_basic_auth to work again.

I won't need to go digging into the rails internals now, though, to understand the problem.