WCF REST is not returning a Vary response header when media type is negotiated
I have a simple WCF REST service:
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class Service1
{
[WebGet(UriTemplate = "{id}")]
public SampleItem Get(string id)
{
return new SampleItem() { Id = Int32.Parse(id), StringValue = "Hello" };
}
}
There is not constrain about the media that the service should return.
When I send a request specifying json
format, it returns JSON:
GET http://localhost/RestService/4 HTTP/1.1
User-Agent: Fiddler
Accept: application/json
Host: localhost
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 30
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sun, 02 Oct 2011 18:06:47 GMT
{"Id":4,"StringValue":"Hello"}
When I specify xml
, it returns XML:
GET http://localhost/RestService/4 HTTP/1.1
User-Agent: Fiddler
Accept: application/xml
Host: localhost
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 194
Content-Type: application/xml; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sun, 02 Oct 2011 18:06:35 GMT
<SampleItem xmlns="http://schemas.datacontract.org/2004/07/RestPrototype.Service" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Id>4</Id><StringValue>Hello</StringValue></SampleItem>
So far so good, the problem is that the service doesn't return a Vary
HTTP header to say that the content has been negotiated and that the Accept
http header has been a determinant factor.
Should not it be like this?:
GET http://localhost/RestService/4 HTTP/1.1
User-Agent: Fiddler
Accept: application/json
Host: localhost
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 30
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Vary:Accept
Date: Sun, 02 Oct 2011 18:06:47 GMT
{"Id":4,"StringValue":"Hello"}
As far as I know, in terms of caching, the "Vary" header will tell intermediate caches that the response is generated based on the URI and the Accept
HTTP header. Otherwise, a proxy could cache a json response, and use it for somebody that is asking xml.
There is any way to make WCF REST put this hea开发者_运维问答der automatically?
Thanks.
You can use a custom message inspector to add the Vary
header to the responses. Based on the automatic formatting rules for WCF WebHTTP, the order is 1) Accept header; 2) Content-Type of request message; 3) default setting in the operation and 4) default setting in the behavior itself. Only the first two are dependent on the request (thus influencing the Vary
header), and for your scenario (caching), only GET are interesting, so we can discard the incoming Content-Type as well. So writing such an inspector is fairly simple: if the AutomaticFormatSelectionEnabled
property is set, then we add the Vary: Accept
header for the responses of all GET requests - the code below does that. If you want to include the content-type (for non-GET requests as well), you can modify the inspector to look at the incoming request as well.
public class Post_0acbfef2_16a3_440a_88d6_e0d7fcf90a8e
{
[DataContract(Name = "Person", Namespace = "")]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
}
[ServiceContract]
public class MyContentNegoService
{
[WebGet(ResponseFormat = WebMessageFormat.Xml)]
public Person ResponseFormatXml()
{
return new Person { Name = "John Doe", Age = 33 };
}
[WebGet(ResponseFormat = WebMessageFormat.Json)]
public Person ResponseFormatJson()
{
return new Person { Name = "John Doe", Age = 33 };
}
[WebGet]
public Person ContentNegotiated()
{
return new Person { Name = "John Doe", Age = 33 };
}
[WebInvoke]
public Person ContentNegotiatedPost(Person person)
{
return person;
}
}
class MyVaryAddingInspector : IEndpointBehavior, IDispatchMessageInspector
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
WebHttpBehavior webBehavior = endpoint.Behaviors.Find<WebHttpBehavior>();
if (webBehavior != null && webBehavior.AutomaticFormatSelectionEnabled)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
HttpRequestMessageProperty prop;
prop = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
if (prop.Method == "GET")
{
// we shouldn't cache non-GET requests, so only returning this for such requests
return "Accept";
}
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
string varyHeader = correlationState as string;
if (varyHeader != null)
{
HttpResponseMessageProperty prop;
prop = reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;
if (prop != null)
{
prop.Headers[HttpResponseHeader.Vary] = varyHeader;
}
}
}
}
public static void SendGetRequest(string uri, string acceptHeader)
{
SendRequest(uri, "GET", null, null, acceptHeader);
}
public static void SendRequest(string uri, string method, string contentType, string body, string acceptHeader)
{
Console.Write("{0} request to {1}", method, uri.Substring(uri.LastIndexOf('/')));
if (contentType != null)
{
Console.Write(" with Content-Type:{0}", contentType);
}
if (acceptHeader == null)
{
Console.WriteLine(" (no Accept header)");
}
else
{
Console.WriteLine(" (with Accept: {0})", acceptHeader);
}
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
req.Method = method;
if (contentType != null)
{
req.ContentType = contentType;
Stream reqStream = req.GetRequestStream();
byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
reqStream.Write(bodyBytes, 0, bodyBytes.Length);
reqStream.Close();
}
if (acceptHeader != null)
{
req.Accept = acceptHeader;
}
HttpWebResponse resp;
try
{
resp = (HttpWebResponse)req.GetResponse();
}
catch (WebException e)
{
resp = (HttpWebResponse)e.Response;
}
Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
foreach (string headerName in resp.Headers.AllKeys)
{
Console.WriteLine("{0}: {1}", headerName, resp.Headers[headerName]);
}
Console.WriteLine();
Stream respStream = resp.GetResponseStream();
Console.WriteLine(new StreamReader(respStream).ReadToEnd());
Console.WriteLine();
Console.WriteLine(" *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* ");
Console.WriteLine();
}
public static void Test()
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
ServiceHost host = new ServiceHost(typeof(MyContentNegoService), new Uri(baseAddress));
ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(MyContentNegoService), new WebHttpBinding(), "");
endpoint.Behaviors.Add(new WebHttpBehavior { AutomaticFormatSelectionEnabled = true });
endpoint.Behaviors.Add(new MyVaryAddingInspector());
host.Open();
Console.WriteLine("Host opened");
foreach (string operation in new string[] { "ResponseFormatJson", "ResponseFormatXml", "ContentNegotiated" })
{
foreach (string acceptHeader in new string[] { null, "application/json", "text/xml", "text/json" })
{
SendGetRequest(baseAddress + "/" + operation, acceptHeader);
}
}
Console.WriteLine("Sending some POST requests with content-nego (but no Vary in response)");
string jsonBody = "{\"Name\":\"John Doe\",\"Age\":33}";
SendRequest(baseAddress + "/ContentNegotiatedPost", "POST", "text/json", jsonBody, "text/xml");
SendRequest(baseAddress + "/ContentNegotiatedPost", "POST", "text/json", jsonBody, "text/json");
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
}
In WCF Web API, we are planning to add automatically setting the Vary header during conneg. For now if you are using Web API, you can do this by either using a custom operation handler or message handler. For WCF HTTP then using a message inspector as Carlos recommended is the way to go.
It seems the webHttpBinding was designed to fit the model described in this post which allows soap to "co-exists" with non-soap endpoints. The implication in the endpoint URLs of the code in that link is each endpoint provides a resource as a single content-type. The endpoints in that link are configured to support soap, json and plain XML through the endpointBehaviors attribute.
Your sample shows that webHttpBinding can support content negotiation but it is only partially implemented since the Vary header isn't being generated by WCF. If you want to use a framework that embraces the REST architecture style more closely, look at the reasons you might want to use OpenRasta.
That behavior IMHO violates the SHOULD in https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-p6-cache-16#section-3.5 . I cannot see any grounds not to send the Vary in case of a negotiated response.
I'll send it to the WCF HTTP list for clarification/fixing and get back with the answer here.
Jan
精彩评论