require "time" # for Time.httpdate
# All html, text, css, and csv content should be compressed
# Only vector graphics and uncompressed bitmaps can benefit from compression.
#GIF, JPG, and PNG already use a lz* algorithm, and certain browsers can get confused.
"application/x-font-ttf",
"application/x-font-opentype",
"application/vnd.ms-fontobject",
# All javascript should be compressed
"application/ecmascript",
"application/javascript",
# All xml should be compressed
# Creates Rack::Deflater middleware.
# [app] rack app instance
# [options] hash of deflater options, i.e.
# 'min_length' - minimum content length to trigger deflating (defaults to 1024 bytes)
# 'skip_if' - a lambda which, if evaluates to true, skips deflating
# 'include_types' - a lambda (Ruby 1.9+) or an array denoting mime-types to compress
def initialize(app, options = {})
@min_length = options[:min_length] || 1024
@skip_if = options[:skip_if]
@include_types = options[:include_types] || DEFAULT_CONTENT_TYPES
status, headers, body = @app.call(env)
headers = Rack::Utils::HeaderHash.new(headers)
unless should_deflate?(env, status, headers, body)
return [status, headers, body]
request = Rack::Request.new(env)
encoding = Rack::Utils.select_best_encoding(%w(gzip deflate identity),
# Set the Vary HTTP header.
vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
unless vary.include?("*") || vary.include?("Accept-Encoding")
headers["Vary"] = vary.push("Accept-Encoding").join(",")
headers['Content-Encoding'] = "gzip"
headers.delete('Content-Length')
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
[status, headers, GzipStream.new(body, mtime)]
headers['Content-Encoding'] = "deflate"
headers.delete('Content-Length')
[status, headers, DeflateStream.new(body)]
body.close if body.respond_to?(:close)
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
[406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
def initialize(body, mtime)
gzip =::Zlib::GzipWriter.new(self)
@body.close if @body.respond_to?(:close)
Zlib::DEFAULT_COMPRESSION,
# drop the zlib header which causes both Safari and IE to choke
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
@body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
@body.close if @body.respond_to?(:close)
def should_deflate?(env, status, headers, body)
# Skip compressing empty entity body responses and responses with
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
# Skip if response body is too short
if @min_length > headers['Content-Length'].to_i
# Skip if :skip_if lambda is provided and evaluates to true
@skip_if.call(env, status, headers, body)
mime_type = headers['Content-Type'].gsub(/;.*\Z/,"").downcase
# Skip if :include is provided and evaluates to false
!((@include_types === mime_type) ||
(@include_types.respond_to?(:"include?") &&
@include_types.include?(mime_type)))
puts "Not compressing #{mime_type}"