Over a million developers have joined DZone.

Serving Compressed Rails Assets from S3 via Cloudfront

DZone's Guide to

Serving Compressed Rails Assets from S3 via Cloudfront

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

I wrote about how we do asset packaging with Rails, how we Jammit and push them to S3 in this post. We’ve had a few surprises since then, one that had to do with compressed assets.

If a browser sends an Accept-Encoding: gzip header for a resource that has both an uncompressed and a compressed copy (eg. client.js and client.js.gz), the server can respond with the compressed version of the file along with the original Content-Type (eg. text/css) and Content-Encoding: gzip headers. This is called content negotiation. Unfortunately S3 has very poor negotiation skills. CloudFront CDN does better, but only if the server behind it supports it. So putting a CloudFront in front of S3 produces the same effect and Amazon recommends using a custom origin server (a fancy word for another web server), other than S3 if you want to enable content negotiation. We’d rather not serve static assets from Heroku because it requires checking them into source control. And we’d rather not add a web server that we have to maintain – too much infrastructure when multiplied by the number of developers.

The solution is, as usual, to monkey-patch extend rails. We’re going to tell our Rails application to serve a compressed file if the browser includes the right headers by rendering an URL to the compressed version.

In Rails 3.0 (your mileage will vary for 3.1) paths to assets are written via ActionView::Helpers::AssetTagHelper’s path_to_javascript and path_to_stylesheet. We can figure out browser capabilities by examining request.env['HTTP_ACCEPT_ENCODING'] and rewrite those URLs to our liking.

module ActionView
  module Helpers
    module AssetTagHelper
      def accept_encoding?(encoding)
        (request.env['HTTP_ACCEPT_ENCODING'] || '').split(',').include?(encoding)
      def rewrite_path_to_gzip?(source)
        (! config.asset_host.blank?) and (source =~ /assets\//) and accept_encoding?('gzip')
      def path_to_javascript(source)
        source = rewrite_path_to_gzip(source) if rewrite_path_to_gzip?(source)
        compute_public_path(source, 'javascripts', 'js')
      def path_to_stylesheet(source)
        source = rewrite_path_to_gzip(source) if rewrite_path_to_gzip?(source)
        compute_public_path(source, 'stylesheets', 'css')
      def rewrite_path_to_gzip(source)
        source + ".cgz"

config/initializers/asset_tag_helper.rb and spec/initializers/rails/asset_tag_helper_spec.rb

The .cgz extension replaces the .gz extension to workaround a bug in Safari. The rewrite_path_to_gzip? check ensures that we’re rendering something under assets and that we’re using an external server (not in development). Your condition may be different.

Finally, we must set the proper content encoding headers when pushing the assets to S3. Here’s the meat of our Rake task.

    File.open(entry) do |entry_file|
      content_options = {}
      content_type = MIME::Types.type_for(entry)[0]
      content_options['x-amz-acl'] = 'public-read'
      if entry.ends_with?('.gz')
        uncompressed_entry = entry[0..-4]
        entry = "#{uncompressed_entry}.cgz"
        content_type = MIME::Types.type_for(uncompressed_entry)[0]
        content_options['content-encoding'] = 'gzip'
      content_options['content-type'] = content_type.to_s
      key = 'assets/'
      key += entry.slice(from.length + 1, entry.length - from.length - 1)
      s3i.put(to, key, entry_file, content_options)

Note that the Content-Type for a .css.gz file is the same as for a .css file (text/css) and that Content-Encoding is set to gzip.

To verify that your implementation worked, examine the page source and make sure you got a .css.cgz link for stylesheets. Then, navigate to the .css.cgz URL – it should display an uncompressed CSS.

For purists, this implementation is really bad. A browser may request a web page with Accept-Encoding: gzip, but then request the CSS from another server without it. I am going to decide that this never happens in real life.

Source: http://code.dblock.org/serving-compressed-rails-assets-from-s3-via-cloudfront

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.


Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}