2010-12-01 9 views
5

Estoy trabajando en una pequeña aplicación C#/WPF que interactúa con un servicio web implementado en Ruby on Rails, utilizando llamadas hechas a mano HttpWebRequest y serialización JSON. Sin almacenamiento en caché, todo funciona como debería y también tengo la autenticación y compresión HTTP funcionando.HttpWebRequest con almacenamiento en caché habilitado arroja excepciones

Una vez que habilito el almacenamiento en caché, estableciendo request.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheIfAvailable);, las cosas salen mal, en el entorno de producción. Cuando me conecto a una instancia simple de WEBrick, las cosas funcionan bien, recibo HTTP/1.1 304 Not Modified como se esperaba y HttpWebRequest entrega el contenido en caché.

Cuando intento lo mismo con el servidor de producción, ejecutando nginx/0.8.53 + Phusion Passenger 3.0.0, la aplicación se rompe. La primera solicitud (no guardada) se sirve correctamente, pero en la segunda solicitud que da como resultado la respuesta 304, obtengo un WebException que indica que "La solicitud se anuló: la solicitud se canceló." tan pronto como invoco request.GetResponse().

He pasado las conexiones a través de fiddler, que no ha ayudado mucho; tanto WEBrick como nginx devuelven un cuerpo de entidad vacío, aunque con diferentes encabezados de respuesta. Interceptar la solicitud y cambiar los encabezados de respuesta para nginx para que coincida con los de WEBrick no cambió nada, lo que me lleva a pensar que podría tratarse de un tema para mantener vivo; Sin embargo, la configuración request.KeepAlive = false; no cambia nada: no interrumpe las cosas cuando se conecta a WEBrick, y no arregla nada cuando se conecta a nginx.

Por lo que vale la pena, la WebException.InnerException es una NullReferenceException con las siguientes StackTrace:

at System.Net.HttpWebRequest.CheckCacheUpdateOnResponse() 
at System.Net.HttpWebRequest.CheckResubmitForCache(Exception& e) 
at System.Net.HttpWebRequest.DoSubmitRequestProcessing(Exception& exception) 
at System.Net.HttpWebRequest.ProcessResponse() 
at System.Net.HttpWebRequest.SetResponse(CoreResponseData coreResponseData) 

encabezados para la conexión (de trabajo) WEBrick:

########## request 
GET /users/current.json HTTP/1.1 
Authorization: Basic *REDACTED* 
Content-Type: application/json 
Accept: application/json 
Accept-Charset: utf-8 
Host: testbox.local:3030 
If-None-Match: "84a49062768e4ca619b1c081736da20f" 
Accept-Encoding: gzip, deflate 
Connection: Keep-Alive 
########## response 
HTTP/1.1 304 Not Modified 
X-Ua-Compatible: IE=Edge 
Etag: "84a49062768e4ca619b1c081736da20f" 
Date: Wed, 01 Dec 2010 18:18:59 GMT 
Server: WEBrick/1.3.1 (Ruby/1.8.7/2010-08-16) 
X-Runtime: 0.177545 
Cache-Control: max-age=0, private, must-revalidate 
Set-Cookie: *REDACTED* 

cabeceras para el (a excepción de lanzamiento de) conexión nginx:

########## request 
GET /users/current.json HTTP/1.1 
Authorization: Basic *REDACTED* 
Content-Type: application/json 
Accept: application/json 
Accept-Charset: utf-8 
Host: testsystem.local:8080 
If-None-Match: "a64560553465e0270cc0a23cc4c33f9f" 
Accept-Encoding: gzip, deflate 
Connection: Keep-Alive 
########## response 
HTTP/1.1 304 Not Modified 
Connection: keep-alive 
Status: 304 
X-Powered-By: Phusion Passenger (mod_rails/mod_rack) 3.0.0 
ETag: "a64560553465e0270cc0a23cc4c33f9f" 
X-UA-Compatible: IE=Edge,chrome=1 
X-Runtime: 0.240160 
Set-Cookie: *REDACTED* 
Cache-Control: max-age=0, private, must-revalidate 
Server: nginx/0.8.53 + Phusion Passenger 3.0.0 (mod_rails/mod_rack) 

ACTUALIZACIÓN:

He intentado hacer una caché ETag Manual rápido y sucio-, pero resulta que es un no-voy: tengo un WebException al invocar request.GetResponce(), diciéndome que "El servidor remoto devolvió un error: (304) No modificado. " - sí, .NET, lo sabía un poco, y me gustaría (intentar) manejarlo yo mismo, grr.

ACTUALIZACIÓN 2:

Cada vez más cerca de la raíz del problema. Parece que la diferencia es la diferencia en los encabezados de respuesta para la solicitud inicial . WEBrick incluye un encabezado Date: Wed, 01 Dec 2010 21:30:01 GMT, que no está presente en la respuesta nginx. También hay otras diferencias, pero al interceptar la respuesta inicial de nginx con el violín y agregar un encabezado Date, los siguientes HttpWebRequest s pueden procesar las respuestas (no modificadas) nginx 304.

Vamos a tratar de buscar una solución alternativa, así como a obtener nginx para agregar el encabezado de fecha.

Actualización 3:

Parece que el problema es con serverside Phusion de pasajeros, tienen un open issue por la falta de la cabecera Date. Aún diría que el comportamiento de HttpWebRequest es ... subóptimo.

ACTUALIZACIÓN 4:

añadido un billete Microsoft Connect para el error.

+0

Hola! Creo que has encontrado un error en HttpWebRequest. ¿Puedes crear un registro de la reproducción (ver http://ferozedaud.blogspot.com/2009/08/tracing-with-systemnet.html) y crear una solicitud de error en http: // connect? De esa forma, Microsoft puede solucionar el problema. – feroze

+0

@feroze: ayer me quedé sin tiempo, pero ahora he agregado el error en Connect. – snemarch

Respuesta

1

Por lo tanto, resulta ser Phusion de pasajeros (o nginx, dependiendo de cómo se mire - Delgado y también) que no agrega un Date HTTP Encabezado de respuesta, combinado con lo que veo como un error en .NET HttpWebRequest (en mi situación no hay If-Modified-Since, por lo tanto, la fecha no debería ser necesaria), lo que ocasiona el problema.

El solución para este caso particular era editar nuestra rieles ApplicationController:

class ApplicationController < ActionController::Base 
    # ...other stuff here 

    before_filter :add_date_header 
    # bugfix for .NET HttpWebRequst 304-handling bug and various 
    # webservers' lazyness in not adding the Date: response header. 
    def add_date_header 
     response.headers['Date'] = Time.now.to_s 
    end 
end 

ACTUALIZACIÓN:

Resulta que es un poco más complejo que "simplemente" establecer HttpRequestCachePolicy - para repro, también necesito tener HTTP Basic Auth construido manualmente. Por lo tanto, los componentes involucrados son los siguientes:

  1. servidor HTTP que no incluye un encabezado de respuesta HTTP "Fecha:".
  2. construcción manual del encabezado de solicitud de autorización HTTP.
  3. uso de HttpRequestCachePolicy.

más pequeño repro he sido capaz de llegar a:

namespace Repro 
{ 
    using System; 
    using System.IO; 
    using System.Net; 
    using System.Net.Cache; 
    using System.Text; 

    class ReproProg 
    { 
     const string requestUrl = "http://drivelog.miracle.local:3030/users/current.json"; 

     // Manual construction of HTTP basic auth so we don't get an unnecessary server 
     // roundtrip telling us to auth, which is what we get if we simply use 
     // HttpWebRequest.Credentials. 
     private static void SetAuthorization(HttpWebRequest request, string _username, string _password) 
     { 
      string userAndPass = string.Format("{0}:{1}", _username, _password); 
      byte[] authBytes = Encoding.UTF8.GetBytes(userAndPass.ToCharArray()); 
      request.Headers["Authorization"] = "Basic " + Convert.ToBase64String(authBytes); 
     } 

     static public void DoRequest() 
     { 
      var request = (HttpWebRequest) WebRequest.Create(requestUrl); 

      request.Method = "GET"; 
      request.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheIfAvailable); 
      SetAuthorization(request, "[email protected]", "12345678"); 

      using(var response = request.GetResponse()) 
      using(var stream = response.GetResponseStream()) 
      using(var reader = new StreamReader(stream)) 
      { 
       string reply = reader.ReadToEnd(); 
       Console.WriteLine("########## Server reply: {0}", reply); 
      } 
     } 

     static public void Main(string[] args) 
     { 
      DoRequest(); // works 
      DoRequest(); // explodes 
     } 
    } 
} 
1

Creo que a los diseñadores les parece razonable lanzar una excepción cuando el "comportamiento esperado" --- es decir, obtener un cuerpo de respuesta --- no se puede completar. Puede manejar esto un poco inteligente de la siguiente manera:

catch (WebException ex) 
{ 
    if (ex.Status == WebExceptionStatus.ProtocolError) 
    { 
     var statusCode = ((HttpWebResponse)ex.Response).StatusCode; 
     // Test against HttpStatusCode enumeration. 
    } 
    else 
    { 
     // Do something else, e.g. throw; 
    } 
} 
+0

Puede valer la pena intentarlo, pero parece ser un hack particularmente desagradable, especialmente porque (cuando trato de hacer el almacenamiento en caché manualmente), obtengo la excepción ya en GetResponse(), antes de que el evento intente obtener el flujo de respuesta. Además, sería mejor si pudiera obtener el caché automático trabajando en nginx, pero gracias de todos modos :) – snemarch

Cuestiones relacionadas