Arreglando MSIE con un middleware de Rails

En otro de los alardes de simpatía de Internet Explorer, el otro día nos topamos con este conocido problema Explorer

El comportamiento de Explorer a la hora de establecer la cabecera HTTP_ACCEPT: la primera vez que carga una URL pondrá la muy extraña cabecera

image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, 
application/x-shockwave-flash, application/vnd.ms-excel,
application/vnd.ms-powerpoint, application/msword, */*

mientras que las posteriores peticiones llevaran un escueto */*. Por qué tiene MSIE 6 la costumbre de solicitar tantos tipos de contenido MIME pero no un básico text/html se me escapa (y más aún por qué en posteriores peticiones cambia la cabecera) pero seguro que hay una razón perfectamente justificada para ello.

En todo caso esta curiosa manía de MSIE puede hacer que se rompan cosas en Rails. En concreto, una aplicación con un controlador de esta guisa devolverá el contenido incorrecto a MSIE:

def index
  respond_to do |format|
   format.xml { ... }
   format.html { ... }
  end
end

dado que Rails no recibe explícitamente la petición de devolver HTML, asumirá que la petición se satisface correctamente devolviendo XML y por tanto Internet Explorer recibirá nuestra vista XML cuando el usuario simplemente quería cargar una URL.

Por supuesto, la solución trivial es reescribir el código

def index
  respond_to do |format|
   format.html { ... }
   format.xml { ... }
  end
end

pero esto no siempre es tan fácil: en nuestro caso, estábamos usando el excelente plugin inherited_resources de José Valim, y la búsqueda no era trivial (aunque desde luego al final se encontró y resolvió de esta manera)

Pero me quedé con la duda de si no sería posible blindarse contra este tipo de escenarios...

Los middlewares al rescate

Como contaba el otro día los middlewares nos permiten manipular el entorno de una petición HTTP y su respuesta justo antes de inyectársela a Rails. Así que parecen un candidato ideal para lo que queríamos hacer, a saber: dada una petición HTTP, detectar si el navegador que la envía es MSIE y, si en la cabecera HTTP_ACCEPT aparece el contenido MIME */* la manipularemos para insertar justo antes text/html

Veamos cómo. En primer lugar, crearemos nuestro middleware en, por ejemplo, lib/fix_msie_accept_header.rb

class FixMSIEAcceptHeader
  def initialize(app, logger = nil)
    @app = app
  end

  def call(env)
    if is_msie?(env) && !has_format?(env)
        env['HTTP_ACCEPT'] = 'text/html ,' + env['HTTP_ACCEPT']
    end

    status, headers, response = @app.call(env)

    [status, headers, response ]
  end

  def is_msie?(env)
    env['HTTP_USER_AGENT'] =~ /MSIE/
  end

  def has_format?(env)
    !(env['HTTP_ACCEPT'].include?("*/*"))
  end
end

¡ya está! Ahora solamente tenemos que decirle a Rails que cargue nuestro middleware, por ejemplo en config/environment.rb

Rails::Initializer.run do |config|
  # Settings in config/environments/* take precedence over those specified here.
  # Application configuration should go into files in config/initializers
  # -- all .rb files in that directory are automatically loaded.

  # Add additional load paths for your own custom dirs
  # config.load_paths += %W( #{RAILS_ROOT}/extras )

  config.middleware.use "FixMSIEAcceptHeader"
  ...
end

Podemos comprobar que está todo listo ejecutando rake middleware en el raíz de nuestra aplicación:

use Rack::Lock
use ActionController::Failsafe
use ActionController::Reloader
use ActionController::Session::CookieStore, #<Proc:0x01af6a08@(eval):8>
use ActionController::RewindableInput
use ActionController::ParamsParser
use Rack::MethodOverride
use Rack::Head
use FixMSIEAcceptHeader
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
run ActionController::Dispatcher.new

como se ve, en la versión 2.3 mucha de la funcionalidad clásica de Rails se encuentra ahora implementada como middlewares (por ejemplo el primero es el mutex global que impide la ejecución concurrente del framework) Estos middlewares forman una cadena por la cual va pasando la petición HTTP hacia el framework y por la que volverá la respuesta, dando la oportunidad para que cada middleware haga su labor. Tenemos la posibilidad de configurar nuestros middlwares en la posición que más nos convenga.

blog comments powered by Disqus