IE6 Accept Header is Faulty and Makes format.any Suck May 14, 2008
Posted by reidmix in AbstractRequest, ActionController, Changeset, Code, Example, MimeResponds, Monkey Patch, Plugins & Gems, Rails, acts_as_authenticated.Tags: Accept Header, Any, Format, Format.any, HTTP_ACCEPT, ie, ie bug, ie6, ie6 bug, internet explorer, internet explorer 6
trackback
I’ve been using the awesome acts_as_authenticated plugin as the basis for user login and authorization. Its access_denied method takes advantage of the ActionController::MimeResponds.any method which will be invoked on a login_required before filter:
def access_denied
respond_to do |format|
format.html do
store_location
redirect_to new_session_path
end
format.any do
request_http_basic_authentication 'Web Password'
end
end
end
This code essentially says, if it’s an html request, store the original request and redirect to the login page, any other requests get the 401 Unauthorized header which the user agent can then use to initiate an HTTP Auth. This bit of code only works correctly with Edge Rail’s 8987 Changeset that allows any to act as a catch-all for any request mime-types not already specified.
Awesome!
Except when Internet Explorer 6 comes into the picture and happily sends this
strange and incomplete ‘Accept:’ header:
image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
As you can see, it doesn’t specify application/xhtml+xml or even text/html and what’s worse is the subsequent requests to the same page sends the catch-all */*.
What does this all mean?
When IE6 navigates to a page that requires login, instead of redirecting the user to the login page, the user is presented with an HTTP Auth Dialog. What’s worse is that in my application, cookies and other application state for an web-based end-user is set outside of the login_from_basic_auth method — you know — in my login action.
My guess is when the format.any fix goes out in the next rails release, IE6 users on acts_as_authenticated sites are going to be sad-faced. I’m not entirely sure the correct way to fix this problem but this is how I solved it in my application:
class ActionController::AbstractRequest
def accepts_with_faulty_header
@env['HTTP_ACCEPT']='*/*' if @env['HTTP_USER_AGENT'] =~ /msie/i and @env['HTTP_USER_AGENT'] !~ /opera|webtv/i
accepts_without_faulty_header
end
alias_method_chain :accepts, :faulty_header
end
Originally, I thought to patch the Mime::Type.parse function, but decided that I wanted to make sure to look at the user agent for IE6. As you can see, I switch the HTTP_ACCEPT header to the catch-all */* string if the original HTTP_ACCEPT header is a match to the exact (bizarre) IE6 string and the HTTP_USER_AGENT is IE (matches MSIE but not Opera or WebTV). Then I call the original ActionController::AbstractRequest.accepts method.
I’d love to hear your ideas and suggestions — and if others have run into this problem as I didn’t find many users have this exact problem and how they may have solved it.
Update: Based on the conversation in the comments, I’ve re-written the patch to always change the HTTP_ACCEPT header to the catch-all */* if it is an IE Browser.
Checking for an exact string is prone to failure, as there’s no guarantee that IE6 will send the Accept tokens in that order, and/or that all of these tokens will be present (e.g. the following application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword are only sent when those applications are installed).
You can not rely on the Accept header (in any version of IE) to determine whether or not the request represents a top-level navigation.
Good to know, I wondered if certain versions of IE6 only sent those headers specifically.
So would you suggest I normalize all IE6 HTTP_ACCEPT headers to be the catch-all?
I’m still feeling unsure about normalizing the header to the catch-all rather than knowing what IE6′s true capabilities are via the Accept header.
I’m not entirely sure I understand the scenario of what you’re trying to do here (likely because I don’t know anything about Rails).
The Accept header in IE is one of two values: */*, or a string made up from the values of the “Accepted Documents” key in the system registry. That key is accessible to anything installed on the system, so some applications (e.g. Office) and add-ons (Flash/Silverlight, etc) shove their own values in there.
AFAIK, rails will look at the Accept header and find the first match between the list and the types of files that can be delivered. Lets say there is an action that can respond (deliver) the follow formats:
def action respond_to do |format| format.html { # render html } format.xml { # render xml } format.any { # render json } end endIf you have an Accept header that has text/html, application/xml, application/json, */*, the action will deliver html.
If you have an Accept header that has application/xml, text/html, application/json, */*, the action will deliver xml (first priority in the Accept list that matches).
If you have an Accept header that has text/plain, */* then the format.any will be selected and deliver json (impractical but good for my example).
And lastly if you just have */* as the Accept header, the action will deliver the first match in the action’s respond_to, in this case, html.
Because, IE is not specifying html, xhtml in it’s list but is specifying a bunch of other formats or just */*, then in the first case it will deliver text (format.any) and in the latter case it will deliver html (correctly).
If I cannot rely on the Accept header and the header does not list html or xhtml, then I would say if I know the user agent is MSIE I should normalize it’s Accept header to be the catch-all */* if it is not already.
When I tried this on edge rails it didn’t fix the IE6 problem. I fixed it in the lib/authenticated_system.rb file. I did the following to fix it:
def access_denied
ml)
+ request.format = :html if request.env['HTTP_USER_AGENT'] =~ /msie/i && (request.format.to_s =~ /(text|html|xml|js|\*)/).nil?
respond_to do |format|
format.html do
store_location
redirect_to new_sessions_path
end
# format.any doesn’t work in rails version < http://dev.rubyonrails.org/changeset/8987
# you may want to change format.any to e.g. format.any(:js,
format.any do
request_http_basic_authentication ‘Web Password’
end
end
end
My copy of IE6 was sending a type through. It was “image/gif” and for some reason the above code didn’t fire and the http_auth dialog was displayed.
With my code, it works properly. What do you think?
Best of luck!
[...] cu primul post. Nu sunteti primii, toata lumea a observat ca Microsoft are probleme… ( vezi aici [...]
[...] solução para esse problema encontramos nesse link que está em inglês. Com base nessa solução, fizemos (na verdade, o Rodrigo fez) um initializer [...]
Looks like they turned of the Accept header in Rails 2.2:
http://guides.rubyonrails.org/2_2_release_notes.html section 6.3
The problem still occurs in Rails 2.3.2
If you change “ActionController::AbstractRequest” to “ActionController::Request” in the fix you will be good to go.
Rails 2.3 swapped out CGI requests for Rack requests and removed the “Abstract” part of the class name.
[...] browser compatibility I should favor the extension approach. And I’m not surprised it’s Microsoft’s [...]