Sam Elliott home

Rails ❤ Middleware - Part 2

05 Jul 2010

Continuing my series on middleware, I am now starting to go through the individual pieces of the Rails Middleware stack.

ActionDispatch::Static

in rails/actionpack/lib/action_dispatch/middleware/static.rb

This middleware does two things. Firstly if the file actually exists, it serves it back to the user using Rack::File. This totally bypasses the rest of the Rails stack.

Secondly, if the file does not exist, it does a few checks to look for files that it has cached with caches_page. To do this, it starts by looking for a directory matching the path. If this exists, it looks for a file called index.ext inside it. If the directory does not exist, it adds .ext to the end of the path and calls the Rack::File with this altered path. .ext is not the actual extension it puts on - instead it takes the extension from ActionController::Base.page_cache_extension and uses that. By default this is the “.html” that we know and love.

Failing all these checks for files that it can serve, it forwards everything onwards down the stack.

module ActionDispatch
  class Static
    FILE_METHODS = %w(GET HEAD).freeze

    def initialize(app, root)
      @app = app
      @file_server = ::Rack::File.new(root)
    end

    def call(env)
      path   = env['PATH_INFO'].chomp('/')
      method = env['REQUEST_METHOD']

      if FILE_METHODS.include?(method)
        if file_exist?(path)
          return @file_server.call(env)
        else
          cached_path = directory_exist?(path) ? "#{path}/index" : path
          cached_path += ::ActionController::Base.page_cache_extension

          if file_exist?(cached_path)
            env['PATH_INFO'] = cached_path
            return @file_server.call(env)
          end
        end
      end

      @app.call(env)
    end

    private
      def file_exist?(path)
        full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path))
        File.file?(full_path) && File.readable?(full_path)
      end

      def directory_exist?(path)
        full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path))
        File.directory?(full_path) && File.readable?(full_path)
      end
  end
end

Rack::Lock

from rack/lib/rack/lock.rb

This merely makes the whole stack threadsafe by implementing a Mutex lock on requests going through the stack. Presumably if you can make your stack threadsafe without this, you can remove it to have more than one request at a time going through your application.

module Rack
  class Lock
    FLAG = 'rack.multithread'.freeze

    def initialize(app, lock = Mutex.new)
      @app, @lock = app, lock
    end

    def call(env)
      old, env[FLAG] = env[FLAG], false
      @lock.synchronize { @app.call(env) }
    ensure
      env[FLAG] = old
    end
  end
end

ActiveSupport::Cache::Strategy::LocalCache

from rails/activesupport/lib/active_support/cache/strategy/local_cache.rb

Rails does some whacky-complicated metaprogramming here to insert a not-thread-safe in-memory store into the stack. I’m unsure as to what exactly it does, except for override its name to make it harder to find.

Rack::Runtime

from rack/lib/rack/runtime.rb

This needs no more explanation than is given in its docs.

module Rack
  # Sets an "X-Runtime" response header, indicating the response
  # time of the request, in seconds
  #
  # You can put it right before the application to see the processing
  # time, or before all the other middlewares to include time for them,
  # too.
  class Runtime
    def initialize(app, name = nil)
      @app = app
      @header_name = "X-Runtime"
      @header_name << "-#{name}" if name
    end

    def call(env)
      start_time = Time.now
      status, headers, body = @app.call(env)
      request_time = Time.now - start_time

      if !headers.has_key?(@header_name)
        headers[@header_name] = "%0.6f" % request_time
      end

      [status, headers, body]
    end
  end
end

Up Next

Fork me on GitHub