我正在开发一个小型的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缓存,但事实证明这是不行的:我在调用WebException
时得到request.GetResponce()
,告诉我“远程服务器返回错误:(304)Not Modified。“ - 是的,.NET,我有点知道,我想(尝试)自己处理它,grr。
更接近问题的根源。 showstopper似乎是初始请求的响应标头的差异。 WEBrick包含一个Date: Wed, 01 Dec 2010 21:30:01 GMT
标头,该标头在nginx回复中不存在。还有其他差异,但是使用fiddler拦截初始nginx回复并添加Date
标头,后续HttpWebRequest
s能够处理(未修改的)nginx 304回复。
尝试寻找解决方法,以及让nginx添加Date标头。
似乎服务器端问题是Phusion Passenger,他们有open issue缺少Date
标头。我仍然说HttpWebRequest
的行为是......不是最理想的。
为错误添加了Microsoft Connect票证。
答案 0 :(得分:1)
我认为设计师认为,当“预期行为” - 即得到一个反应机构 - 无法完成时抛出异常是合理的。您可以按照以下方式智能地处理:
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;
}
}
答案 1 :(得分:1)
所以,事实证明是Phusion Passenger(或nginx,取决于你如何看待它 - 以及瘦身),它不会添加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
要复杂一点 - 对于repro,我还需要手动构建HTTP Basic Auth。所以涉及的组件如下:
我能够提出最小的复制品:
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
}
}
}