我正在开发一个小型 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 HttpWebRequest
s 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.
发布评论
评论(2)
我认为设计者认为当“预期行为”(即获取响应主体)无法完成时抛出异常是合理的。您可以按如下方式巧妙地处理此问题:
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:
所以,事实证明是 Phusion Passenger (或 nginx,取决于你如何看待它 - 还有 Thin),它没有添加
Date
HTTP 响应标头,结合我所看到的.NET HttpWebRequest 中的错误(在我的情况下没有If-Modified-Since
,因此 Date 不是必需的)导致了该问题。这种特殊情况的解决方法是编辑我们的Rails ApplicationController:
更新:
事实证明它比“只是”设置要复杂一点
HttpRequestCachePolicy
- 为了重现,我还需要手动构建 HTTP Basic Auth。因此,涉及的组件如下:我能想到的最小的复制品:
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 noIf-Modified-Since
, thus Date shouldn't be necessary) leading to the problem.The workaround for this particular case was to edit our Rails ApplicationController:
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:Smallest repro I've been able to come up with: