Skip to content

Commit

Permalink
Use ETag for conditional requests. Fixes #2.
Browse files Browse the repository at this point in the history
The URIs and ETags of all GitHub API responses that are used to construct the feed are hashed together to make the ETag for the feed itself.

This doesn't save any processing time on the server, but benefits the client by allowing it to easily reuse its own cached data.
  • Loading branch information
bgrainger committed Apr 14, 2012
1 parent b97af79 commit facbad5
Showing 1 changed file with 41 additions and 10 deletions.
51 changes: 41 additions & 10 deletions src/GitHubFeeds/Controllers/CommentsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.ServiceModel.Syndication;
using System.Text;
using System.Text.RegularExpressions;
Expand All @@ -23,14 +24,15 @@ public void ListAsync(ListParameters p)
{
AsyncManager.OutstandingOperations.Increment();

ListData data = new ListData { RequestETag = Request.Headers["If-None-Match"] };
Uri uri = CreateUri(p, 1, 1);
HttpWebRequest request = CreateRequest(p, uri);
request.GetHttpResponseAsync()
.ContinueWith(t => GetCommentCount(p, t))
.ContinueWith(t => GetCommentCount(t))
.ContinueWith(t => GetCommentPages(p, t.Result))
.ContinueWith(t => Task.Factory.ContinueWhenAll(t.Result, ts => GetComments(p, ts))).Unwrap()
.ContinueWith(t => Task.Factory.ContinueWhenAll(t.Result, ts => GetComments(data, ts))).Unwrap()
.ContinueWith(t => CreateFeed(p, t.Result))
.ContinueWith(t => CreateResult(p, t))
.ContinueWith(t => CreateResult(data, t))
.ContinueWith(t =>
{
AsyncManager.Parameters["result"] = t.Result;
Expand Down Expand Up @@ -66,7 +68,7 @@ private static HttpWebRequest CreateRequest(ListParameters p, Uri uri)
}

// Gets the number of comments for a repo.
private static int GetCommentCount(ListParameters p, Task<HttpWebResponse> responseTask)
private static int GetCommentCount(Task<HttpWebResponse> responseTask)
{
string linkHeader;
using (HttpWebResponse response = responseTask.Result)
Expand Down Expand Up @@ -120,8 +122,11 @@ private static Task<HttpWebResponse>[] GetCommentPages(ListParameters p, int com
}

// Merges the comments returned from multiple HTTP requests and returns the last 50.
private static List<GitHubComment> GetComments(ListParameters p, Task<HttpWebResponse>[] tasks)
private static List<GitHubComment> GetComments(ListData data, Task<HttpWebResponse>[] tasks)
{
// concatenate all the response URIs and ETags; we will use this to build our own ETag
var responseETags = new List<string>();

// download comments as JSON and deserialize them
List<GitHubComment> comments = new List<GitHubComment>();
foreach (Task<HttpWebResponse> task in tasks)
Expand All @@ -131,6 +136,11 @@ private static List<GitHubComment> GetComments(ListParameters p, Task<HttpWebRes
if (response.StatusCode != HttpStatusCode.OK)
throw new ApplicationException("GitHub server returned " + response.StatusCode);

// if the response has an ETag, add it to the list of all ETags
string eTag = response.Headers[HttpResponseHeader.ETag];
if (!string.IsNullOrEmpty(eTag))
responseETags.Add(response.ResponseUri.AbsoluteUri + ":" + eTag);

// TODO: Use asynchronous reads on this asynchronous stream
// TODO: Read encoding from Content-Type header; don't assume UTF-8
using (Stream stream = response.GetResponseStream())
Expand All @@ -139,6 +149,21 @@ private static List<GitHubComment> GetComments(ListParameters p, Task<HttpWebRes
}
}

// if each response had an ETag, build our own ETag from that data
if (responseETags.Count == tasks.Length)
{
// concatenate all the ETag data
string eTagData = responseETags.Join("\n");

// hash it
byte[] md5;
using (MD5 hash = MD5.Create())
md5 = hash.ComputeHash(Encoding.UTF8.GetBytes(eTagData));

// the ETag is the quoted MD5 hash
data.ResponseETag = "\"" + string.Join("", md5.Select(by => by.ToString("x2", CultureInfo.InvariantCulture))) + "\"";
}

return comments
.OrderByDescending(c => c.created_at)
.Take(50)
Expand Down Expand Up @@ -187,19 +212,25 @@ private static string CreateCommentHtml(GitHubComment comment)
HttpUtility.HtmlEncode(comment.commit_id.Substring(0, 8)), comment.body_html);
}

private ActionResult CreateResult(ListParameters p, Task<SyndicationFeed> feedTask)
private ActionResult CreateResult(ListData data, Task<SyndicationFeed> feedTask)
{
if (feedTask.IsFaulted)
return new HttpStatusCodeResult((int) HttpStatusCode.InternalServerError, feedTask.Exception.Message);

SyndicationFeed feed = feedTask.Result;
Response.AppendHeader("Last-Modified", feed.LastUpdatedTime.ToString("r"));
if (data.ResponseETag != null)
Response.AppendHeader("ETag", data.ResponseETag);

DateTime ifModifiedSince;
if (DateTime.TryParse(Request.Headers["If-Modified-Since"], out ifModifiedSince) && ifModifiedSince >= feed.LastUpdatedTime)
if (data.RequestETag == data.ResponseETag)
return new HttpStatusCodeResult((int) HttpStatusCode.NotModified);

return new SyndicationFeedAtomResult(feedTask.Result);
}

// ListData contains private state that needs to be communicated between the various async methods invoked by List.
private sealed class ListData
{
public string RequestETag { get; set; }
public string ResponseETag { get; set; }
}
}
}

0 comments on commit facbad5

Please sign in to comment.