Almacenamiento en caché de ASP.NET MVC e IE: la manipulación de los encabezados de respuesta es ineficaz

Fondo

Estoy intentando ayudar a un colega a resolver un problema que no ha sido un problema en los últimos 6 meses. Después de la implementación más reciente de una aplicación ASP.NET MVC 2, FileResult respuestas de FileResult que obligan a un archivo PDF al usuario a abrir o guardar tienen problemas para existir lo suficiente en el equipo cliente para que el lector de PDF las abra.

Las versiones anteriores de IE (especialmente 6) son los únicos navegadores afectados. Firefox y Chrome y las versiones más nuevas de IE (> 8) se comportan como se esperaba. Con esto en mente, la siguiente sección define las acciones necesarias para recrear el problema.

Comportamiento

  1. El usuario hace clic en un enlace que apunta a un método de acción (un hipervínculo simple con un atributo href ).
  2. El método de acción genera un PDF representado como una secuencia de bytes. El método siempre recrea el PDF.
  3. En el método de acción, los encabezados se configuran para instruir a los navegadores sobre cómo almacenar en caché la respuesta. Son:

     response.AddHeader("Cache-Control", "public, must-revalidate, post-check=0, pre-check=0"); response.AddHeader("Pragma", "no-cache"); response.AddHeader("Expires", "0"); 

    Para aquellos que no están familiarizados con exactamente lo que hacen los encabezados :

    a. Cache-Control: public

    Indica que la respuesta PUEDE ser almacenada en caché por cualquier caché, incluso si normalmente no sería cacheable o cacheable solo dentro de un caché no compartido.

    segundo. Cache-Control: debe revalidar

    Cuando la directiva debe revalidarse está presente en una respuesta recibida por un caché, ese caché NO DEBE utilizar la entrada después de que quede obsoleta para responder a una solicitud posterior sin antes revalidarla con el servidor de origen.

    do. Cache-Control: pre-check (introducido con IE5)

    Define un intervalo en segundos después del cual se debe verificar la frescura de una entidad. La comprobación puede realizarse después de que se muestre el recurso al usuario, pero se asegura de que en el próximo viaje de ida y vuelta la copia en caché estará actualizada.

    re. Cache-Control: post-check (introducido con IE5)

    Define un intervalo en segundos después del cual se debe verificar la frescura de una entidad antes de mostrarle al usuario el recurso.

    mi. Pragma: no-cache (para garantizar la compatibilidad con versiones anteriores de HTTP / 1.0)

    Cuando la directiva de no caché está presente en un mensaje de solicitud, una aplicación DEBERÍA reenviar la solicitud al servidor de origen, incluso si tiene una copia en caché de lo que se solicita.

    F. Vence

    El campo Expirar encabezado de entidad proporciona la fecha / hora después de la cual la respuesta se considera obsoleta.

  4. Devolvemos el archivo de la acción

     return File(file, "mime/type", fileName); 
  5. Al usuario se le presenta un cuadro de diálogo Abrir / Guardar

  6. Hacer clic en “Guardar” funciona como se esperaba, pero al hacer clic en “Abrir” se inicia el lector de PDF, pero el archivo temporal IE almacenado ya se ha eliminado cuando el lector intenta abrir el archivo, por lo que se queja de que el archivo falta (y es).

Hay media docena de otras aplicaciones aquí que usan los mismos encabezados para forzar a Excel, CSV, PDF, Word y un montón de otros contenidos a los usuarios y nunca ha habido un problema.

La pregunta

  • ¿Los encabezados son correctos para lo que estamos tratando de hacer? Queremos que el archivo exista temporalmente (en caché), pero siempre será reemplazado por nuevas versiones, aunque las solicitudes sean idénticas).

Los encabezados de respuesta se establecen en el método de acción antes de devolver un FileResult . Le pedí a mi colega que intentara crear una nueva clase que FileResult de FileResult y en su lugar anule el método ExecuteResult para que modifique los encabezados y luego base.ExecuteResult() lugar – no hay estado en eso.

Tengo la corazonada de que el encabezado “Expira” de “0” es el culpable. De acuerdo con este artículo del W3C , establecerlo en “0” implica que “ya expiró”. Quiero que caduque, solo que no quiero que IE vaya a eliminarlo del sistema de archivos antes de que la aplicación que lo maneja tenga la oportunidad de abrirlo.

Como siempre, gracias!

Editar: la solución

Tras realizar más pruebas (utilizando Fiddler para inspeccionar los encabezados), vimos que los encabezados de respuesta que creíamos que estaban siendo configurados no eran los que el navegador estaba interpretando. Como yo no estaba familiarizado con el código, no tenía conocimiento de un problema subyacente: los encabezados eran pisoteados fuera del método de acción.

No obstante, voy a dejar abierta esta pregunta. Todavía sobresale esto: parece haber alguna discrepancia entre el encabezado Expires tiene un valor de 0 contra -1 . Si alguien puede reclamar diferencias por diseño, con respecto a IE , aún me gustaría saber de eso. Sin embargo, en cuanto a una solución, los encabezados anteriores funcionan según lo previsto con el valor Expires en -1 en todos los navegadores.

Actualización 1

La publicación ¿Cómo controlar el almacenamiento en caché de la página web en todos los navegadores? describe en detalle que se puede evitar el almacenamiento en caché en todos los navegadores con la ayuda de configurar Expires = 0. Todavía no estoy vendido en este argumento de 0 vs -1

Creo que deberías simplemente usar

 HttpContext.Current.Response.Cache.SetMaxAge (new TimeSpan (0)); 

o

 HttpContext.Current.Response.Headers.Set ("Cache-Control", "private, max-age=0"); 

para establecer max-age=0 que significa nada más como la validación de caché (ver aquí ). Si se configura adicionalmente ETag en el encabezado con alguna sum de comprobación personalizada de hash de los datos, la ETag de la solicitud anterior se enviará al servidor. El servidor puede devolver los datos o, en caso de que los datos sean exactamente los mismos que antes, puede devolver el cuerpo vacío y HttpStatusCode.NotModified como el código de estado. En el caso de que el navegador web obtenga los datos del caché del navegador local.

Te recomiendo usar Cache-Control: private que obliga a dos cosas importantes: 1) desactivar el almacenamiento en caché de los datos en el proxy, que a veces tiene configuraciones de caché muy agresivas 2) permitirá el almacenamiento en caché de los datos, pero no permitirá compartir de la memoria caché con otros usuarios. Puede resolver problemas de privacidad porque los datos que usted devuelve a un usuario no pueden leer otros usuarios. Por cierto, el código HttpContext.Current.Response.Cache.SetMaxAge (new TimeSpan (0)) establece Cache-Control: private, max-age=0 en el encabezado HTTP de forma predeterminada. Si desea usar Cache-Control: public , puede usar SetCacheability (HttpCacheability.Public); para sobrescribir el comportamiento o usar Headers.Set lugar de Cache.SetMaxAge .

Si tiene interés en estudiar más opciones de almacenamiento en caché del protocolo HTTP, le recomendaría que lea el tutorial de almacenamiento en caché .

ACTUALIZADO : decido escribir algo más de información para aclarar mi posición. Corresponde a la información de la Wikipedia, incluso navegadores web tan antiguos como Mosaic 2.7, Netscape 2.0 e Internet Explorer 3.0 admiten marzo de 1996, pre-estándar de HTTP / 1.1 descrito en RFC 2068. Así que supongo (pero no lo pruebo) que el viejo los navegadores web admiten max-age=0 encabezado HTTP. De cualquier forma, Netscape 2.06 e Internet Explorer 4.0 definitivamente son compatibles con HTTP 1.1.

Entonces, primero debería preguntarle: ¿qué estándares de HTML usa? ¿Todavía usas HTML 2.0 en lugar de más tarde HTML 3.2 publicado en enero de 1997? Supongo que utiliza al menos HTML 4.0 publicado en diciembre de 1997. Por lo tanto, si crea su aplicación al menos en HTML 4.0, su sitio puede orientarse en los clientes web que admiten HTTP 1.1 e ignorar (no admitir) los clientes web que no es compatible con HTTP 1.1.

Ahora sobre otros encabezados “Cache-Control” como “privado, max-age = 0”. Incluir los encabezados es, en mi opinión, pura paranoia . Como tengo algún problema de almacenamiento en caché intenté también incluir otros encabezados diferentes, pero después de leer detenidamente la sección 14.9 de RFC2616 utilizo solo “Cache-Control: private, max-age = 0”.

El único encabezado “Cache-Control” que se puede analizar adicionalmente es “must-revalidate” descrito en la sección 14.9.4 al que hice referencia anteriormente. Aquí está la cita:

La directiva must-revalidate es necesaria para admitir el funcionamiento confiable de ciertas características del protocolo. En todos los casos, un caché HTTP / 1.1 DEBE obedecer la directiva must-revalidate; en particular, si la memoria caché no puede llegar al servidor de origen por algún motivo, DEBE generar una respuesta 504 (tiempo de espera de puerta de enlace).

Los servidores DEBERÍAN enviar la directiva de must-revalidate si y solo si el hecho de no revalidar una solicitud en la entidad podría dar como resultado una operación incorrecta, como una transacción financiera silenciosamente no ejecutada. Los destinatarios NO DEBEN realizar ninguna acción automatizada que viole esta directiva y NO DEBEN proporcionar automáticamente una copia no validada de la entidad si falla la revalidación.

Aunque esto no se recomienda, los agentes de usuario que operan bajo severas restricciones de conectividad PUEDEN violar esta directiva pero, de ser así, DEBEN advertir explícitamente al usuario de que se ha proporcionado una respuesta no validada. La advertencia DEBE ser proporcionada en cada acceso no validado, y DEBERÍA requerir una confirmación explícita del usuario.

En algún momento, si tengo un problema con la conexión a Internet, veo la página vacía con el mensaje “Gateway Timeout”. Viene del uso de la directiva “must-revalidate”. No creo que el mensaje “Gateway Timeout” realmente ayude al usuario.

Entonces, las personas, cómo prefieren comenzar un procedimiento autodestructivo si oye una señal de “Ocupado” en la llamada a su jefe, deben usar adicionalmente la directiva “deben-revalidar” en el encabezado “Control de caché”. Otras personas que recomiendo solo usan “Cache-Control: private, max-age = 0” y nada más.

Sé que esto es tarde, pero este enlace podría ser de gran ayuda para todos los interesados ​​en el tema: http://dotnet.dzone.com/articles/output-caching-aspnet-mvc

Para IE, recuerdo haber tenido que configurar Expires: -1 . Cómo evitar el almacenamiento en caché en Internet Explorer parece confirmar esto con el siguiente fragmento de código.

 <% Response.CacheControl = "no-cache" %> <% Response.AddHeader "Pragma", "no-cache" %> <% Response.Expires = -1 %> 

Mirando hacia atrás en el código, esto es lo que encontré. Además, recuerdo vagamente que si configura Cache-Control: private es posible que Cache-Control: private no se comporte correctamente con SSL.

 Response.AddHeader("Cache-Control", "no-cache"); Response.AddHeader("Expires", "-1"); 

Además, entonces, no quieres guardar en caché, ¿eh? menciona -1 , pero usa métodos en Response.Cache :

 // Stop Caching in IE Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache); // Stop Caching in Firefox Response.Cache.SetNoStore(); 

Sin embargo, el problema de caché de página ASP (IE8) dice que este código no funciona.