HttpWebRequest с включенным кэшированием выдает исключения - PullRequest
5 голосов
/ 01 декабря 2010

Я работаю над небольшим приложением C # / WPF, которое взаимодействует с веб-службой, реализованной в Ruby on Rails, с использованием вызовов HttpWebRequest ручной работы и сериализации JSON.Без кэширования все работает, как положено, и у меня также работает аутентификация и сжатие HTTP.

Как только я включаю кэширование, установив request.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheIfAvailable);, все идет не так, как надо - в производственной среде.При подключении к простому экземпляру WEBrick все работает нормально, я получаю HTTP/1.1 304 Not Modified как положено и HttpWebRequest доставляет кэшированное содержимое.

Когда я пытаюсь сделать то же самое на производственном сервере, запустив nginx /0.8.53 + Phusion Passenger 3.0.0 , приложение ломается.Первый запрос (не кэшированный) обслуживается должным образом, но при втором запросе, который приводит к ответу 304, я получаю WebException о том, что « Запрос был прерван: запрос был отменен. », как толькоЯ вызываю request.GetResponse().

Я провел соединения через fiddler , что не очень помогло;WEBrick и nginx возвращают пустое тело сущности, хотя и разные заголовки ответа.Перехват запроса и изменение заголовков ответов для nginx так, чтобы они соответствовали заголовкам WEBrick, ничего не изменили, что привело меня к мысли, что это может быть проблемой поддержания активности;установка request.KeepAlive = false; ничего не меняет, хотя - он не ломает вещи при подключении к WEBrick, и не исправляет вещи при подключении к nginx.

Для чего стоит, WebException.InnerException - это NullReferenceException со следующими 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)

Заголовки для (рабочего) соединения 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*

Заголовки для (исключения-соединения) соединения 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)

ОБНОВЛЕНИЕ:

Я пытался сделать быстрый и грязный ручной кэш ETag, но оказалось, что это не пойдет: я получаю WebException при вызове request.GetResponce(), говоря мнечто "Удаленный сервер возвратил ошибку: (304) Не изменено." - да, .NET, я вроде это знал, и я хотел бы (попытаться) справиться с этим сам, grr.

ОБНОВЛЕНИЕ 2:

Получение ближе к корню проблемы.Похоже, что showtopper представляет собой разницу в заголовках ответов для исходного запроса.WEBrick содержит заголовок Date: Wed, 01 Dec 2010 21:30:01 GMT, которого нет в ответе nginx.Есть и другие отличия, но, перехватывая исходный ответ nginx с помощью fiddler и добавляя заголовок Date, последующие HttpWebRequest s могут обрабатывать (неизмененные) ответы nginx 304.

Собираемся попытатьсяищите обходной путь, а также заставьте nginx добавить заголовок Date.

UPDATE 3:

Кажется, что проблема на стороне сервера связана с Phusion Passenger, у них есть открытая проблема об отсутствии заголовка Date.Я бы все еще сказал, что поведение HttpWebRequest ... неоптимально.

ОБНОВЛЕНИЕ 4:

Добавлен Microsoft Connect билет для ошибки.

Ответы [ 2 ]

1 голос
/ 02 декабря 2010

Итак, оказывается, что это Phusion Passenger (или nginx, в зависимости от того, как вы на него смотрите - и Thin также), который не добавляет заголовок ответа Date HTTP, в сочетании с тем, что я вижу как ошибку в .NET HttpWebRequest (в моей ситуации нет If-Modified-Since, поэтому Дата не требуется), что приводит к проблеме.

Обходной путь для В этом конкретном случае было редактирование нашего Rails 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

UPDATE:

Оказывается, это немного сложнее, чем настройка just HttpRequestCachePolicy - чтобы воспроизвести, мне также необходимо создать HTTP Basic Auth, созданный вручную. Таким образом, участвуют следующие компоненты:

  1. HTTP-сервер, который не содержит заголовок ответа HTTP «Date:».
  2. ручное построение заголовка запроса HTTP-авторизации.
  3. использование HttpRequestCachePolicy.

Самая маленькая репродукция, которую я смог придумать:

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, "user@domain.com", "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 голос
/ 01 декабря 2010

Я думаю, что дизайнеры считают разумным выдавать исключение, когда «ожидаемое поведение» - т.е. получение тела ответа - не может быть завершено.Вы можете справиться с этим несколько разумно следующим образом:

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;
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...