WCF REST is not returning a Vary response header when media type is negotiated
Asked Answered
S

4

4

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 header automatically?

Thanks.

Shrum answered 3/10, 2011 at 13:14 Comment(0)
H
4

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();
    }
}
Houstonhoustonia answered 4/10, 2011 at 0:39 Comment(1)
So long, it's hard to find the part that actually adds the header. Can it be used to add other response headers, like 'Access-Control-Allow-Origin'Halflength
D
4

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.

Deprivation answered 4/10, 2011 at 1:8 Comment(0)
C
1

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.

Chimene answered 3/10, 2011 at 15:4 Comment(3)
I would not only say they don't embraces the REST architecture style closely, I would say they don't follow the HTTP protocol. I will take a look to that project, but unfortunately I am forced to use WCF REST. Thanks!Shrum
Unfortunately, asserting the RESTfulness (or not) of WCF can start a religious war these days. WCF does abstract the communication transport (HTTP and others) to satisfy its design goal of separating service implementation code from so-called "plumbing" code. This design limits the built-in support for HTTP as an application platform. It is in that sense that I recommended OpenRasta. Since you're staying with WCF, the new WCF Web API which is focused on HTTP may be an alternate solution for you.Chimene
With the WCF Web API happens exactly the same. Anyway I think I'm going to use it, it looks very interesting. Thanks!Shrum
V
1

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

Vinita answered 3/10, 2011 at 16:11 Comment(2)
Thanks a lot! Where is that list?, just curious :DShrum
I dunno if helpful, but I have tried with the new WCF Web API and happens exactly the same.Shrum

© 2022 - 2024 — McMap. All rights reserved.