1

I am trying to get the "if-modified-since" header to work with my WCF web service.

When a user makes a request to my service, I add an ETag to the outgoing response that contains the timestamp of the request as follows:

var tag = String.Format("\"{0:o}\"", new DateTimeOffset(DateTime.Now));

This results in the following ETag header:

ETag: "2011-10-27T13:09:39.6242263-04:00"

I then take this value and echo it back as the if-modified-since header for subsequent requests like this:

If-Modified-Since:2011-10-27T13:09:39.6242263-04:00

When I examine WebOperationContext.Current.Headers.IfModifiedSince, I never get the value provided. The value is fixed at "12/31/1969 7:00:00 PM".

What am I doing wrong?

UPDATE

I should add that using Fiddler, I can set any value to the If-Modified-Since header and still get the same 1969 value in code.

SonOfPirate
  • 5,642
  • 3
  • 41
  • 97
  • Can you show how you're adding the ETag to your outgoing response? There's a note on MSDN that it's preferred to use the SetETag() method of the OutgoingResponse to ensure its properly quoted, rather than adding it directly to the headers or ETag property. – Tim Oct 27 '11 at 17:30
  • Not sure how that matters since the ETag is being sent in the response correctly. My problem is receiving a request that has a value for the if-modified-since header. I only show the ETag code to show how the date/time value used for the if-modified-since header was obtained. – SonOfPirate Oct 27 '11 at 18:46
  • Is it a coincidence that the value you see in the receiving web method is the epoch -04:00 and your ETag ends with "-04:00"? :) Are you `DateTime.Pars`ing somewhere? – bzlm Oct 27 '11 at 20:56
  • We are using DateTimeOffset to generate the ETag because our clients may be in different time zones from the server and we want to ensure the date reflects server time when sent back in the if-modified-since header. – SonOfPirate Oct 28 '11 at 01:41
  • @SonOfPirate, that wasn't my question. :) – bzlm Oct 29 '11 at 14:51

1 Answers1

2

First off, If-Modified-Since is about conditional GETs regarding the time of the last modification of the resource, while ETag is about conditional GETs regarding an identifier of the resources, so please be careful with mixing the two concepts.

The correct way of implementing support for If-Modified-Since in a WCF service is to use the CheckConditionalRetrieve passing a DateTime value in the WebOperationContext.Current.IncomingRequest object - see the code below for that. If the value of the IMS header is before the date you pass to CheckConditionalRetrieve, the method will exit at that point returning a 304 (Not Modified) response. Otherwise it will just continue. The code below shows that.

Another issue: even through the date format you're using (ISO 8601) works, but it's not correct based on the specification (section 14.25 in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html, and section 3.3.1 in http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1), so you should consider using a valid format to prevent future problems.

You can find a good post about conditional GET support in WCF at http://blogs.msdn.com/b/endpoint/archive/2010/02/25/conditional-get-and-etag-support-in-wcf-webhttp-services.aspx.

public class StackOverflow_7919718
{
    [ServiceContract]
    public class Service
    {
        [WebGet(ResponseFormat = WebMessageFormat.Json)]
        public string GetData()
        {
            Console.WriteLine("If-Modified-Since header (1): {0}", WebOperationContext.Current.IncomingRequest.IfModifiedSince);
            WebOperationContext.Current.IncomingRequest.CheckConditionalRetrieve(DateTime.UtcNow);
            return "Data";
        }
    }

    public static void Test()
    {
        string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
        WebServiceHost host = new WebServiceHost(typeof(Service), new Uri(baseAddress));
        host.Open();
        Console.WriteLine("Host opened");

        Console.WriteLine("Not sending If-Modified-Since header (should return 200):");
        Util.SendRequest(baseAddress + "/GetData", "GET", null, null);

        Console.WriteLine("Sending data in the past, ISO 8601 format (should return 200):");
        Util.SendRequest(baseAddress + "/GetData", "GET", null, null,
            new Dictionary<string, string> { { "If-Modified-Since", "2011-10-25T13:09:39.6242263-04:00" } });

        Console.WriteLine("Sending data in the future, ISO 8601 format (should return 304):");
        Util.SendRequest(baseAddress + "/GetData", "GET", null, null,
            new Dictionary<string, string> { { "If-Modified-Since", "2021-10-25T13:09:39.6242263-04:00" } });

        Console.WriteLine("Sending data in the past, RFC 1123 format (should return 200):");
        Util.SendRequest(baseAddress + "/GetData", "GET", null, null,
            new Dictionary<string, string> { { "If-Modified-Since", "Wed, 26 Oct 2011 01:00:00 GMT" } });

        Console.WriteLine("Sending data in the future, RFC 1123 format (should return 304):");
        Util.SendRequest(baseAddress + "/GetData", "GET", null, null,
            new Dictionary<string, string> { { "If-Modified-Since", "Mon, 27 Oct 2031 10:00:00 GMT" } });

        Console.Write("Press ENTER to close the host");
        Console.ReadLine();
        host.Close();
    }
}
public static class Util
{
    public static string SendRequest(string uri, string method, string contentType, string body)
    {
        return SendRequest(uri, method, contentType, body, null);
    }
    public static string SendRequest(string uri, string method, string contentType, string body, Dictionary<string, string> headers)
    {
        string responseBody = null;

        HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
        req.Method = method;
        if (headers != null)
        {
            foreach (string headerName in headers.Keys)
            {
                switch (headerName)
                {
                    case "If-Modified-Since":
                        req.IfModifiedSince = DateTime.Parse(headers[headerName]);
                        break;
                    default:
                        req.Headers[headerName] = headers[headerName];
                        break;
                }
            }
        }
        if (!String.IsNullOrEmpty(contentType))
        {
            req.ContentType = contentType;
        }

        if (body != null)
        {
            byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
            req.GetRequestStream().Write(bodyBytes, 0, bodyBytes.Length);
            req.GetRequestStream().Close();
        }

        HttpWebResponse resp;
        try
        {
            resp = (HttpWebResponse)req.GetResponse();
        }
        catch (WebException e)
        {
            resp = (HttpWebResponse)e.Response;
        }

        if (resp == null)
        {
            responseBody = null;
            Console.WriteLine("Response is null");
        }
        else
        {
            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();
            if (respStream != null)
            {
                responseBody = new StreamReader(respStream).ReadToEnd();
                Console.WriteLine(responseBody);
            }
            else
            {
                Console.WriteLine("HttpWebResponse.GetResponseStream returned null");
            }
        }

        Console.WriteLine();
        Console.WriteLine("  *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*  ");
        Console.WriteLine();

        return responseBody;
    }
}
carlosfigueira
  • 85,035
  • 14
  • 131
  • 171
  • I appreciate the details. I wondered about the format of the if-modified-since header. Do you think that is the reason the Console.WriteLine statement in the GetData method of your code never returns the value of the header (for me)? – SonOfPirate Oct 28 '11 at 01:49
  • You're saying that when you run this code the WriteLine statement in the server *never* prints out the value of the IMS header? I already tried it in two machines and on the first call it doesn't print out anything, while in the last 4 it prints out the correct value. – carlosfigueira Oct 28 '11 at 03:11
  • No, in my code regardless of the call and what's in the header, I'm always getting "12/31/1969 7:00:00 PM" for the IfModifiedSince property value. I'm generating the request from Fiddler. Your code, when run as-is, works as described. I don't know what the difference is. – SonOfPirate Oct 28 '11 at 12:47
  • Try to look, in Fiddler, to see what's the difference between the request my code sends and the request you send, and see if you can spot something. – carlosfigueira Oct 28 '11 at 15:29
  • @SonOfPirate, if the `IfModifiedSince` property of the request is `DateTime` or `DateTimeOffset`, and you're *always* getting *12/31/1969 7:00:00 PM*, then most likely the `If-Modified-Since` header in the request is unparsable and it's defaulting to the epoch in your time zone (or something). – bzlm Oct 29 '11 at 14:50