Profile faxon

I build websites.

    Understanding how Rack uses Ruby lambdas

    Rack has proven to be a great way for developers to quickly modify the request/response cycle for Ruby web applications.

    Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.

    This article assumes a basic understanding of Ruby lambdas. Robert Sosinski has a good write up on Ruby Blocks, Procs and Lambdas if you need a refresher.

    Ruby lambda and Rack at a glance

    The lambda method converts a block to a Proc that can be invoked with the call method.

    my_lambda = lambda {puts 'Proc Intro'}
    my_lambda.call
    # => Proc Intro
    

    lambdas can also take parameters:

    param_lambda = lambda { |param| puts "Proc Intro: #{param}"}
    param_lambda.call('hello world')
    # => Proc Intro: hello world
    

    This is all fine-and-good, pretty standard Ruby stuff. Rack works well when you start chaining these together. Rack responses return an array of three items: the status, headers, and response body: [status, headers, response_body]. Let's setup a starting point for our response (env) and couple of basic Rack style lambdas.

    env = [200, {"Content-Type" => "text/plain"}, "hello"]
    # => [200, {"Content-Type"=>"text/plain"}, "hello"]
    
    body_lambda = lambda { |env| [200, {"Content-Type" => "text/plain"}, env[2]+" from a proc"] }
    body_lambda.call(env)
    # => [200, {"Content-Type"=>"text/plain"}, "hello from a proc"]
    
    header_lambda = lambda { |env| [200, env[1].merge({"X-Authorization" => "secret"}), env[2]] }
    header_lambda.call(env)
    # => [200, {"Content-Type"=>"text/plain", "X-Authorization"=>"secret"}, "hello"]
    

    Now, to chain them together we just call one with the results of the other.

    env = [200, {"Content-Type" => "text/plain"}, "hello"]
    header_lambda.call(body_lambda.call(env))
    # => [200, {"Content-Type"=>"text/plain", "X-Authorization"=>"secret"}, "hello from a proc"]
    

    Attentive readers will notice that if we run this the other way we get less desirable results:

    body_lambda.call(header_lambda.call(env))
    # => [200, {"Content-Type"=>"text/plain"}, "hello from a proc"]
    

    We lost the header changes because the body_lambda is strictly returning {"Content-Type" => "text/plain"}. This is a gotcha to be aware of when writing middleware. At any point in the chain you can completely reset any part of the return value. You need to be sure to pass on things that should be passed on and only update things your middleware is responsible for. Lets fix body_lambda so that it will pass along any response headers:

    body_lambda = lambda { |env| [200, env[1], env[2]+" from a proc"] }
    

    Now the following two calls will give the same result:

    body_lambda.call(header_lambda.call(env))
    header_lambda.call(body_lambda.call(env))
    

    Rack and other similar tools allow lambdas to be chained together in order to provide a generic framework for handling the request/response. The meat of this is the Enumerable inject method. The inject method will walk over an Enumerable applying a method to the results of the previous iteration and return the final iteration. In our case we pass our env array as a starting point.

    [body_lambda, header_lambda].inject(env) { |app, handler| handler.call(app) }
    # => [200, {"Content-Type"=>"text/plain", "X-Authorization"=>"secret"}, "hello from a proc"]
    

    Unwrapped this is the same as:

    header_lambda.call(body_lambda.call(env))

    Both of our lambdas expect an array to do their work on, so we give them the env array as a starting point. This is important because without it every lambda would have to deal with cases in which there is no value passed.

    Creating classes to use as middleware

    Rack puts chaining lambdas together by providing a wrapper class to build an array of lambdas. Before we get into building a wrapper, let's take a look at how we can build classes that represent our header_lambda and body_lambda from above.

    class Header # new header_lambda
      def initialize(app) @app = app; end
      def call(env)
        status, headers, response = @app.call(env)
        [status, headers.merge({"X-Authorization" => "secret"}), response]
      end
    end
    
    class Body # new body_lambda
      def initialize(app) @app = app; end
      def call(env)
        status, headers, response = @app.call(env)
        [status, headers, "#{response} from Body"]
      end
    end
    
    inital_response_lambda = lambda { |env| [200, {}, "starting"] }
    h = lambda { |app| Header.new(app) }
    h.call(inital_response_lambda)
    # => #<Header:xxx @app=#<Proc:xxx@(irb):1 (lambda)>>
    

    So h represents the lambda containing Header#new, which expects a lambda representing the current state. The response here is the Header class inside lambda { |app| Header.new(app) }. We have to send the call method again to get the response. But notice that Header#call uses @app.call(env) to set the responses. At this point, it doesn't matter what we send the call method.

    h.call(inital_response_lambda).call('go baby go')
    # => [200, {"X-Authorization"=>"secret"}, "starting"]
    

    Why then should we use the call method in the Header or Body classes? Doesn't it seem to add unnecessary confusion? I think it makes the internal code more confusing, but it also makes the API it provides pretty simple. Let's use the inject method used above to put this together.

    lambdas = []
    lambdas << lambda { |app| Header.new(app) }
    lambdas << lambda { |app| Body.new(app) }
    lambdas.inject(inital_response_lambda) { |app, handler| handler.call(app) }
    # => #<Body:xxx @app=#<Header:xxx @app=#<Proc:xxx@header.rb:18 (lambda)>>>
    

    It's not pretty on the console, but the inject method has returned an instance of the Body class containing an instance of Header which in turn contains our inital_response_lambda. Sending call to this with any parameter will use the Body instance to unravel the whole stack.

    Body @app.call('go baby go')
        Header @app.call('go baby go')
            inital_response_lambda.call('go baby go')
            # => returns [200, {}, "starting"]
        Header @app.call([200, {}, "starting"])
        # => returns [200, {"X-Authorization"=>"secret"}, "starting"]
    Body @app.call([200, {"X-Authorization"=>"secret"}, "starting"])
    # => returns [200, {"X-Authorization"=>"secret"}, "starting from Body"]
    

    At this point the whole thing looks like:

    class Header
      def initialize(app) @app = app; end
      def call(env)
        status, headers, response = @app.call(env)
        [status, headers.merge({"X-Authorization" => "secret"}), response]
      end
    end
    
    class Body
      def initialize(app) @app = app; end
      def call(env)
        status, headers, response = @app.call(env)
        [status, headers, "#{response} from Body"]
      end
    end
    
    inital_response_lambda = lambda { |env| [200, {}, "starting"] }
    lambdas = []
    lambdas << lambda { |app| Header.new(app) }
    lambdas << lambda { |app| Body.new(app) }
    
    lambdas.inject(inital_response_lambda) { |app, handler| 
      handler.call(app) }.call('go baby go')
    }
    # => [200, {"X-Authorization"=>"secret"}, "starting from Body"]
    

    Making it easy to use

    Now that we know how to build an array of lambdas and what it takes to run them we can put it all into a class.

    class Builder
      def initialize(&block)
        @lambdas = []
        instance_eval(&block) if block_given?
      end
    
      def use(middleware, *args, &block)
        @lambdas << if block_given?
          lambda { |app| middleware.new(app, *args, &block) }
        else
          lambda { |app| middleware.new(app, *args) }
        end
      end
    
      def to_app
        inital_response_lambda = lambda { |env| [200, {}, ""] }
        @lambdas.inject(inital_response_lambda) { |app, handler| handler.call(app) }
      end
    
      def run
        to_app.call('go baby go') # remember this is thrown away when the call's get back to inital_response_lambda
      end
    end
    

    We can create and run an instance of the builder class like so:

    app = Builder.new {
      use Header
      use Body
    }
    app.run
    # => [200, {"X-Authorization"=>"secret"}, "starting from Body"]
    

    Summary: A look at the internals of how Rack builds middleware for flexible request processing.