启用缓存的 HttpWebRequest 会引发异常

发布于 2024-10-05 02:39:26 字数 3524 浏览 1 评论 0 原文

我正在开发一个小型 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()。

我已经通过 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 缓存,但事实证明这是行不通的:我在调用 request 时得到一个 WebException 。 GetResponce(),告诉我“远程服务器返回错误:(304) 未修改。” - 是的,.NET,我有点知道这一点,而且我想(尝试)自己处理,grr。

更新2:

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

将尝试寻找解决方法,以及让 nginx 添加日期标头。

更新 3:

服务器端问题似乎与 Phusion Passenger 有关,他们有一个

更新 4:

添加了 Microsoft Connect 票证。

I'm working on a small C#/WPF application that interfaces with a web service implemented in Ruby on Rails, using handcrafted HttpWebRequest calls and JSON serialization. Without caching, everything works as it's supposed to, and I've got HTTP authentication and compression working as well.

Once I enable caching, by setting request.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheIfAvailable);, things go awry - in the production environment. When connecting to a simple WEBrick instance, things work fine, I get HTTP/1.1 304 Not Modified as expected and HttpWebRequest delivers the cached content.

When I try the same against the production server, running nginx/0.8.53 + Phusion Passenger 3.0.0, the application breaks. First request (uncached) is served properly, but on the second request which results in the 304 response, I get a WebException stating that "The request was aborted: The request was canceled." as soon as I invoke request.GetResponse().

I've run the connections through fiddler, which hasn't helped a whole lot; both WEBrick and nginx return an empty entity body, albeit different response headers. Intercepting the request and changing the response headers for nginx to match those of WEBrick didn't change anything, leading me to think that it could be a keep-alive issue; setting request.KeepAlive = false; changes nothing, though - it doesn't break stuff when connecting to WEBrick, and it doesn't fix stuff when connecting to nginx.

For what it's worth, the WebException.InnerException is a NullReferenceException with the following 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)

Headers for the (working) WEBrick connection:

########## 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*

Headers for the (exception-throwing) nginx connection:

########## 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)

UPDATE:

I tried doing a quick-and-dirty manual ETag cache, but turns out that's a no-go: I get a WebException when invoking request.GetResponce(), telling me that "The remote server returned an error: (304) Not Modified." - yeah, .NET, I kinda knew that, and I'd like to (attempt to) handle it myself, grr.

UPDATE 2:

Getting closer to the root of the problem. The showstopper seems to be a difference in the response headers for the initial request. WEBrick includes a Date: Wed, 01 Dec 2010 21:30:01 GMT header, which isn't present in the nginx reply. There's other differences as well, but intercepting the initial nginx reply with fiddler and adding a Date header, the subsequent HttpWebRequests are able to process the (unmodified) nginx 304 replies.

Going to try to look for a workaround, as well as getting nginx to add the Date header.

UPDATE 3:

It seems that the serverside issue is with Phusion Passenger, they have an open issue about lack of the Date header. I'd still say that HttpWebRequest's behavior is... suboptimal.

UPDATE 4:

Added a Microsoft Connect ticket for the bug.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

治碍 2024-10-12 02:39:26

我认为设计者认为当“预期行为”(即获取响应主体)无法完成时抛出异常是合理的。您可以按如下方式巧妙地处理此问题:

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;
    }
}

I think the designers find it reasonable to throw an exception when the "expected behavior"---i.e., getting a response body---cannot be completed. You can handle this somewhat intelligently as follows:

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;
    }
}
葬シ愛 2024-10-12 02:39:26

所以,事实证明是 Phusion Passenger (或 nginx,取决于你如何看待它 - 还有 Thin),它没有添加 Date HTTP 响应标头,结合我所看到的.NET HttpWebRequest 中的错误(在我的情况下没有 If-Modified-Since,因此 Date 不是必需的)导致了该问题。

这种特殊情况的解决方法是编辑我们的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

更新:

事实证明它比“只是”设置要复杂一点 HttpRequestCachePolicy - 为了重现,我还需要手动构建 HTTP Basic Auth。因此,涉及的组件如下:

  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, "[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
        }
    }
}

So, it turns out to be Phusion Passenger (or nginx, depending on how you look at it - and Thin as well) that doesn't add a Date HTTP response header, combined with what I see as a bug in .NET HttpWebRequest (in my situation there's no If-Modified-Since, thus Date shouldn't be necessary) leading to the problem.

The workaround for this particular case was to edit our 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:

Turns out it's a bit more complex than "just" setting HttpRequestCachePolicy - to repro, I also need to have manually constructed HTTP Basic Auth. So the involved components are the following:

  1. HTTP server that doesn't include a HTTP "Date:" response header.
  2. manual construction of HTTP Authorization request header.
  3. use of HttpRequestCachePolicy.

Smallest repro I've been able to come up with:

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
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文