A month ago I challenged Simone to a friendly duel about who could get the best YSlow score. I did all sorts of things to improve the score but the one thing I never figured out was how to gzip compress the WebResource.axd handler. Then yesterday I was playing around with Fiddler and noticed that IE doesn’t cache WebResource.axd but loads it at every request to a page. Then I knew I had to do something about it and I might as well give the gzip compression another try.

I already have a HTTP compression module that works like a charm, so I just needed it to compress WebResource.axd and implement the right client cache mechanisms as well. Sounds simple right?

The compression

I knew this was my Achilles heel since I’ve failed at it before. Lucky for me, Miron Abramson just did it a week ago so I took a look at his solution. It was exactly what I needed, so I picked out the parts I needed and stuck them into my own compression module.

What Mirons code does is to create an HTTP request to the WebResource.axd, copy the response to a memory stream, compress it and serve it back to the client. That is a very clever approach but had a big problem. Every time the browser requested the WebResource.axd handler his module created an HTTP request. That was too big an overhead for me. I fixed it easily by caching the response at the first request and then served the compressed bytes from the cache from then on. Apart from that, his code rocks.

The caching

First of all I set the expires, last-modified and ETag HTTP headers that let’s any browser cache the response. Browsers are a little different when it comes to caching web resources so by specifying both an ETag and last-modified header takes care of them all.

Further more I added support for the If-None-Match request header. What it does is that when a page is refreshed by hitting F5 or similar, the browser sends the If-None-Match header to the server with the ETag as the value. Then I can simply check if the incoming ETag value is the same as the server ETag and terminate the response with a 304 Not modified response header.

That saves bandwidth and tells the browser to use the version from its cache instead of loading the content from the server again. The thing is that files served from WebResource.axd are always static so no harm can be done by using all means to let the browser cache them.

Limitations

The module expects that all files served by the WebResource.axd are JavaScript. That means that you probably run into trouble if you serve images or other file types from the WebResource.axd handler. Most websites don’t and I don’t like doing that in my projects, so that’s why I didn’t spend extra time on supporting it.

Download

Get the CompressionModule.cs file below to compress both your pages and WebResource.axd handler.

CompressionModule.zip (2,27 kb)

PS. I won the YSlow challenge.

Recently I’ve needed a method that would look at some text and automatically discover all URLs and turn them into hyperlinks. I’ve done that before so it was a matter of copy/paste. This time it was a little more complicated, because the resolved URLs could not be longer than 50 characters long. That was important because otherwise it would break the design. A long URL doesn’t word wrap so it would end up bleeding out of the design.

So, the challenge was to resolve the URLs and turn them into links, while keeping the anchor text at a max of 50 characters long. To shorten the URL is easy enough, but it all comes down to how you want it shortened.

The rules

1. If the URL is longer than 50 characters then remove “http://”.
2. If it still is longer than allowed it must compress the folder structure like shown below.

http://www.microsoft.com/windows/server/2003/compare.aspx -> http://www.microsoft.com/.../compare.aspx

3. If the URL is still longer, then it must look for query strings and fragments and remove them as well.

The code

[code:c#]

private static readonly Regex regex = new Regex("((http://|www\\.)([A-Z0-9.-:]{1,})\\.[0-9A-Z?;~&#=\\-_\\./]{2,})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly string link = "<a href=\"{0}{1}\">{2}</a>";

public static string ResolveLinks(string body)
{
  if (string.IsNullOrEmpty(body))
    return body; 

  foreach (Match match in regex.Matches(body))
  {
    if (!match.Value.Contains("://"))
    {         
      body = body.Replace(match.Value, string.Format(link, "http://", match.Value, ShortenUrl(match.Value, 50)));
    }
    else
    {
      body = body.Replace(match.Value, string.Format(link, string.Empty, match.Value, ShortenUrl(match.Value, 50)));
    }
  }

  return body;
}

private static string ShortenUrl(string url, int max)
{
  if (url.Length <= max)
    return url;

  // Remove the protocal
  int startIndex = url.IndexOf("://");
  if (startIndex > -1)
    url = url.Substring(startIndex + 3);

  if (url.Length <= max)
    return url;

  // Remove the folder structure
  int firstIndex = url.IndexOf("/") + 1;
  int lastIndex = url.LastIndexOf("/");
  if (firstIndex < lastIndex)
    url = url.Replace(url.Substring(firstIndex, lastIndex - firstIndex), "...");

  if (url.Length <= max)
    return url;

  // Remove URL parameters
  int queryIndex = url.IndexOf("?");
  if (queryIndex > -1)
    url = url.Substring(0, queryIndex);

  if (url.Length <= max)
    return url;

  // Remove URL fragment
  int fragmentIndex = url.IndexOf("#");
  if (fragmentIndex > -1)
    url = url.Substring(0, fragmentIndex);

  if (url.Length <= max)
    return url;

  // Shorten page
  firstIndex = url.LastIndexOf("/") + 1;
  lastIndex = url.LastIndexOf(".");
  if (lastIndex - firstIndex > 10)
  {
    string page = url.Substring(firstIndex, lastIndex - firstIndex);
    int length = url.Length - max + 3;
    url = url.Replace(page, "..." + page.Substring(length));
  }

  return url;
}

[/code]

Implementation

To use these methods, just call the ResolveLinks method like so:

[code:c#]

string body = ResolveLinks(txtComment.Text);

[/code]

It works on URLs with or without the http:// protocol prefix. In other words http://www.example.com/ and http://www.example.com/ resolves to the same URL. This technique is implemented in the comments on this blog. You can test it by writing a comment with a URL in it.