启用了缓存的HttpWebRequest会抛出异常

sne*_*rch 5 c# httpwebrequest

我正在开发一个小型的C#/ WPF应用程序,该应用程序使用手工HttpWebRequest调用和JSON序列化与Ruby on Rails中实现的Web服务进行交互.没有缓存,一切都按预期工作,我也有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().

我通过提琴手进行连接,但这并没有帮助很多; 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)
Run Code Online (Sandbox Code Playgroud)

(工作)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*
Run Code Online (Sandbox Code Playgroud)

(异常抛出)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)
Run Code Online (Sandbox Code Playgroud)

更新:

我尝试做一个快速而又脏的手动ETag缓存,但事实证明这是不行的:我WebException在调用时得到一个request.GetResponce(),告诉我"远程服务器返回错误:(304)Not Modified." - 是的,.NET,我有点知道,而且我想(尝试)自己处理它,grr.

更新2:

更接近问题的根源.showstopper似乎是初始请求的响应头的差异.WEBrick包含一个Date: Wed, 01 Dec 2010 21:30:01 GMT标头,该标头在nginx回复中不存在.还有其他差异,但是用fiddler拦截初始nginx回复并添加Date标题,后续的HttpWebRequests能够处理(未修改的)nginx 304回复.

试图寻找一种解决方法,以及让nginx添加Date标头.

更新3:

似乎服务器端问题是Phusion Passenger,他们有一个关于缺少标题的公开问题Date.我仍然说这种HttpWebRequest行为是......不是最理想的.

更新4:

为该错误添加了Microsoft Connect票证.

sne*_*rch 1

所以,事实证明是 Phusion Passenger(或 nginx,取决于你如何看待它 - 还有 Thin)没有添加DateHTTP 响应标头,再加上我在 .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
Run Code Online (Sandbox Code Playgroud)

更新:

事实证明,它比“仅”设置要复杂一些HttpRequestCachePolicy- 为了重现,我还需要手动构建 HTTP 基本身份验证。所以涉及到的组件如下:

  1. 不包含 HTTP“Date:”响应标头的 HTTP 服务器。
  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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)