A few days ago, I was going to write a complaint about people not putting the Last-Modified and ETag headers on their HTTP Handlers, and/or not putting something rational in their Last-Modified. I was hitting a bunch of RSS and WeblogAPI endpoints with Fiddler, and noticed that many people would put the request time in the Last-Modified header, or that it wouldn't be there at all. If you've done this, you've basically asked for bandwidth problems. It means that the only way for a client app to determine if you've changed anything is for it to re-request everything from that API.
It's a pretty simple matter to add a valid Last-Modifed header to the results of an HTTP Handler (and you're not using an aspx for your RSS or other API endpoints, are you?):
context.Response.Cache.SetLastModified(someDate)';
Notice that I didn't just say, "SetLastModified(DateTime.Now)". Always setting to now means you've just sent a useless Last-Modified header. Odds are you're pulling some data off of disc, or out of a database. Therefore, you should know when the data was actually last modified, don't you? Grab the most recent time any entry was created/updated, or the datetime stamp of the file, or something. Give the client a hint that things have changed. It means they can do code like (I say "like" as I'm sure there's a better solution than swallowing the 304)
WebRequest _req = WebRequest.Create(url);
if (null != _data) {
//add headers for ETag, Last-Modified
if (null != _data.ETag && String.Empty != _data.ETag) {
_req.Headers.Add(HttpRequestHeader.IfNoneMatch, _data.ETag);
}
if (null != _data.LastUpdated) {
((HttpWebRequest)_req).IfModifiedSince = _data.LastUpdated;
}
}
WebResponse _resp = null;
try {
_resp = _req.GetResponse();
StreamReader _r = new StreamReader(_resp.GetResponseStream());
_data.Contents = _r.ReadToEnd();
} catch (WebException wex) {
//if it's a 304, we're OKdOK
//it just means the data hasn't changed
if (WebExceptionStatus.ProtocolError == wex.Status) {
if (HttpStatusCode.NotModified == ((HttpWebResponse)wex.Response).StatusCode) {
//gulp
}
}
}
ETags are a little more difficult, it seems. When I first tried adding an ETag to the output of an HTTP Handler, nothing was added to the output. After a bit of fiddling, I discovered that you needed to set the SetCacheability to either ServerAndNoCache or ServerAndPrivate (Others might work as well).
if (!String.IsNullOrEmpty(context.Request.Headers["If-Modified-Since"])) {
String lastModified = context.Request.Headers["If-Modified-Since"];
DateTime lastModifiedDate = DateTime.Parse(lastModified, CultureInfo.InvariantCulture);
if(lastModifiedDate.CompareTo(fileModifiedDate) < 0) {
isNewRequest = false;
}
}
if (isNewRequest) {
if (!String.IsNullOrEmpty(context.Request.Headers["If-None-Match"])) {
String etag = context.Request.Headers["If-None-Match"];
if (etag.CompareTo(ETAG) == 0) {
isNewRequest = false;
}
}
}
if (isNewRequest) {
//do your stuff here
} else {
context.Response.StatusCode = 304;
}
Now, clients can remember these two values (Last-Modified and ETag) and ask if there are any updates. Similarly, any handlers that I write can see if the client has passed these two (in the form of If-Modified-Since or If-None-Match headers), and return a 304 instead, saving myself a bit of bandwidth.
Print | posted on Tuesday, September 05, 2006 10:56 PM