Daily Code Reading #9 – Rack::StaticFallback

Today I’m looking at another rack middleware from coderack.org called Rack::StaticFallback.

This middleware is useful in development if you have users upload files to an application and you don’t want to always grab copies of those files when you develop locally. Rack::StaticFallback will rewrite requests to those files and use the actual files from the production server. This is how the request/response looks like for a static file:

  1. Web page renders an image “/uploads/user1.png”
  2. Your local web server tries to load the “/uploads/user1.png” file
  3. Since the file doesn’t exist, it sends the request to Rails/Rack
  4. Rack::StaticFallback matches the path and redirects the request to “http://www.productionsite.com/uploads/user1.png”

The Code

coderack.org only listed the gist but I found the full git repository.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
module Rack
  # Bounces or redirects requests to missing static files.
  # Partially inspired by [http://menendez.com/blog/using-django-as-pass-through-image-proxy/](http://menendez.com/blog/using-django-as-pass-through-image-proxy/)
  #
  # I.e. could be useful when you want to run the server with production database locally
  # and have user uploaded content fetched transparently from production site.
  #
  # Options:
  #     :mode - [ :off,
  #               :bounce, # returns 404 to any request to static URL,
  #               :fallback ] # any request to static path is redirected to :fallback_static_url
  #     :static_path_regex - Regexp which matches the path to your static files.
  #                          Along the lines of the Capistrano conventions defaults to `%r{/system/(audios|photos|videos)}`
  #     :fallback_static_url - URL of the production site
  #
  # To use with Rails install as a plugin:
  #
  #     script/plugin install git://github.com/dolzenko/rack-static_fallback.git
  #
  # then add the following to your `config/environments/development.rb`
  #
  #     config.middleware.insert_after ::Rack::Lock,
  #                                    ::Rack::StaticFallback, :mode => :fallback,
  #                                                            :static_path_regex => %r{/system/uploads},
  #                                                            :fallback_static_url => "http://myproductionsite.com/"
  #
 
  class StaticFallback
    def initialize(app, options = {})
      @app = app
      @mode = options[:mode] || :off
      # along the lines of the Capistrano defaults
      @static_path_regex = options[:static_path_regex] || %r{/system/(audios|photos|videos)}
      @fallback_static_url = options[:fallback_static_url] 
    end
 
    def call(env)
      if env["PATH_INFO"] =~ @static_path_regex
        # If we get here that means that underlying web server wasn't able to serve the static file,
        # i.e. it wasn't found.
        case @mode
          when :off
            # pass the request to next middleware, ultimately Rails
            @app.call(env)
 
          when :bounce
            # don't pass the request so that it doesn't hit framework, which
            # speeds up things significantly
            not_found
 
          when :fallback
            if @fallback_static_url
              # redirect request to the production server
              [ 302, { "Location" => ::File.join(@fallback_static_url, env["PATH_INFO"]) }, [] ]
            else
              ActionController::Base.logger.debug "You're using StaticFallback middleware with StaticFallback.mode set to :fallback " < "text/html", "Content-Length" => "0" }, [] ]
    end
  end
end

Review

Other than the configuration in the initialize method, most of the logic for Rack::StaticFallback is in the #call method which has four different branches (five if you count the web server’s case also):

1. Static request with matching file

This is when a static file is requested and the web server is able to send it directly from the filesystem. It isn’t shown in the code because it’s assumed that the web server would handle this before Rack.

2. Static request with mode off

1
2
3
when :off
  # pass the request to next middleware, ultimately Rails
  @app.call(env)

This happens when a request for a static file comes in and Rack::StaticFallback is configured :off. Since it just passes the request off to the rest of the application, the request will be handled by another middleware and probably trigger Rails’ 404 error.

3. Static request with mode bounce

1
2
3
4
when :bounce
  # don't pass the request so that it doesn't hit framework, which
  # speeds up things significantly
  not_found

This option is interesting. It still serves a 404 error like above, but it doesn’t pass the request to the rest of Rack or Rails. This is useful for pages with 100 images and you don’t want to wait for Rails spin up and return 404s for each one.

4. Static request with mode fallback

1
2
3
4
5
6
7
8
9
when :bounce
  if @fallback_static_url
    # redirect request to the production server
    [ 302, { "Location" => ::File.join(@fallback_static_url, env["PATH_INFO"]) }, [] ]
  else
    ActionController::Base.logger.debug "You're using StaticFallback middleware with StaticFallback.mode set to :fallback " <<
            "however StaticFallback.fallback_static_url has not been set."
    not_found
  end

Here is where all the interesting code happens. When the @fallback_static_url is configured, it will redirect the request to that url with the file name appended. This will let your production server serve up the request. If the @fallback_static_url isn’t configured, then a message is logged and a 404 is returned.

Bug: The logging assumes ActionController is used, which could fail in a non-Rails application like Sinatra.

5. Request that doesn’t match the static paths

1
2
3
else
  @app.call(env)
end

The final case happens when the PATH_INFO doesn’t match the static paths. This calls the rest of the Rack stack which would end up being sent to Rails to route. This case would be called for every dynamic case.