Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
60 lines (56 sloc) 2.97 KB
require 'fileutils'
require 'rack'
# Rack::ResponseCache is a Rack middleware that caches responses for successful
# GET requests with no query string to disk or any ruby object that has an
# []= method (so it works with memcached). When caching to disk, it works similar to
# Rails' page caching, allowing you to cache dynamic pages to static files that can
# be served directly by a front end webserver.
class Rack::ResponseCache
# The default proc used if a block is not provided to .new
# It unescapes the PATH_INFO of the environment, and makes sure that it doesn't
# include '..'. If the Content-Type of the response is text/(html|css|xml),
# return a path with the appropriate extension (.html, .css, or .xml).
# If the path ends with a / and the Content-Type is text/html, change the basename
# of the path to index.html.
DEFAULT_PATH_PROC = proc do |env, res|
path = Rack::Utils.unescape(env['PATH_INFO'])
if !path.include?('..') and match = /text\/((?:x|ht)ml|css)/o.match(res[1]['Content-Type'])
type = match[1]
path = "#{path}.#{type}" unless /\.#{type}\z/.match(path)
path = File.join(File.dirname(path), 'index.html') if type == 'html' and File.basename(path) == '.html'
# Initialize a new ReponseCache object with the given arguments. Arguments:
# * app : The next middleware in the chain. This is always called.
# * cache : The place to cache responses. If a string is provided, a disk
# cache is used, and all cached files will use this directory as the root directory.
# If anything other than a string is provided, it should respond to []=, which will
# be called with a path string and a body value (the 3rd element of the response).
# * &block : If provided, it is called with the environment and the response from the next middleware.
# It should return nil or false if the path should not be cached, and should return
# the pathname to use as a string if the result should be cached.
# If not provided, the DEFAULT_PATH_PROC is used.
def initialize(app, cache, &block)
@app = app
@cache = cache
@path_proc = block || DEFAULT_PATH_PROC
# Call the next middleware with the environment. If the request was successful (response status 200),
# was a GET request, and had an empty query string, call the block set up in initialize to get the path.
# If the cache is a string, create any necessary middle directories, and cache the file in the appropriate
# subdirectory of cache. Otherwise, cache the body of the reponse as the value with the path as the key.
def call(env)
res =
if env['REQUEST_METHOD'] == 'GET' and env['QUERY_STRING'] == '' and res[0] == 200 and path =, res)
if @cache.is_a?(String)
path = File.join(@cache, path) if @cache
FileUtils.mkdir_p(File.dirname(path)), 'wb'){|f| res[2].each{|c| f.write(c)}}
@cache[path] = res[2]
Something went wrong with that request. Please try again.