What is Rack and Rack Middlewares (Basis of Ruby Frameworks) ?

Rack is a very thin interface between Ruby frameworks and web servers that support Ruby. What it basically means is that it provides a minimal API for connecting web servers (supporting Ruby) and web frameworks (that implement it).

Basics

Rack is a simple Ruby object that responds to a call() method. The call() method is passed an env variable that is basically a hash containing a lot of header key/value pairs as well as rack-specific variables. Here’s a very simple rack app.

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

require 'rack'

app = Proc.new do |env|
  [200, { 'Content-Type' => 'application/json' }, ['{"response": "OK"}']]
end

Rack::Handler::WEBrick.run app

require 'rack' ? Right! we need to gem install rack. How is proc a rack app ? Because it responds to call()! So any ruby object that responds to a call() method can be a rack app. Here’s a dump of the env variable.

{
  "GATEWAY_INTERFACE"=>"CGI/1.1",
  "PATH_INFO"=>"/",
  "QUERY_STRING"=>"",
  "REMOTE_ADDR"=>"::1",
  "REMOTE_HOST"=>"localhost",
  "REQUEST_METHOD"=>"GET",
  "REQUEST_URI"=>"http://localhost:8080/",
  "SCRIPT_NAME"=>"",
  "SERVER_NAME"=>"localhost",
  "SERVER_PORT"=>"8080",
  "SERVER_PROTOCOL"=>"HTTP/1.1",
  "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/2.0.0/2014-05-08)",
  "HTTP_HOST"=>"localhost:8080",
  "HTTP_CONNECTION"=>"keep-alive",
  "HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
  "HTTP_UPGRADE_INSECURE_REQUESTS"=>"1",
  "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36",
  "HTTP_ACCEPT_ENCODING"=>"gzip, deflate, sdch",
  "HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8",
  "HTTP_COOKIE"=>"_client1_session=ays0eks3WXpycFlGQktkcWR",
  "rack.version"=>[1, 3],
  "rack.input"=>#<StringIO:0x007ff72203e2f8>,
  "rack.errors"=>#<IO:<STDERR>>,
  "rack.multithread"=>true,
  "rack.multiprocess"=>false,
  "rack.run_once"=>false,
  "rack.url_scheme"=>"http",
  "rack.hijack?"=>true,
  "rack.hijack"=>#<Proc:0x007ff72203db78@/Users/rishabhpugalia/.rvm/gems/ruby-2.0.0-p481/gems/rack-1.6.4/lib/rack/handler/webrick.rb:76 (lambda)>,
  "rack.hijack_io"=>nil,
  "HTTP_VERSION"=>"HTTP/1.1",
  "REQUEST_PATH"=>"/"
}

The other important thing is what the call to call() returns which is what is served by the rack app to the requesting client via the server. It has to be a very simple array of 3 elements:

  • Response Status – Must be greater than or equal to 100.
  • Response Headers – Must be a key/value pair and respond to each().
  • Response Body – Must contain strings and respond to each(). It’s generally an array of strings which are concatenated in the response.

What is WEBrick though ? It’s a ruby web server but Rack::Handler::WEBrick is a built-in rack handler for the WEBrick server making it rack-compatible so that we can use it for development purposes.

We can run the same app using the rackup command that comes with rack. The code will require changes (which is mostly trimming it down):

# config.ru
app = proc do |env|
  [200, { 'Content-Type' => 'application/json' }, ['{"response": "OK"}']]
end

run app

File name must be config.ru. You can either run rackup or rackup config.ru to boot up the server. Note we did not have to explicitly call the WEBrick.run() method.

For a different perspective’s sake, let’s write a rack up using Ruby classes.

# config.ru
class RackApp
  def call(env)
    [ 200, {"Content-Type" => "application/json"}, ['{"response": "OK"}'] ]
  end
end

run RackApp.new # ruby object that responds to call()

Pretty self-explanatory!

Just so you know, Rack is the basis of Rails or Sinatra which means internally they’ve an object that implements a call() method which is called and the response is given to a rack-based (compliant) server. So now you know what it means when you read “X is a rack-based app/framework/server”. A rack-based server will need to contain a handler that conforms to Rack’s specifications.

Routing

Using Rack::Builder‘s map instance method we can implement simple routing too!

class RackApp
  def call(env)
    [ 200, {"Content-Type" => "application/json"}, ['{"response": "route1"}'] ]
  end
end

class AnotherRackApp
  def call(env)
    [ 200, {"Content-Type" => "application/json"}, ['{"response": "route2"}'] ]
  end
end

map '/route1' do
  run RackApp.new
end

map '/route2' do
  run AnotherRackApp.new
end

Serving Static Files and Directories

Static files can also be served via Rack pretty easily. Let’s have the following files in the same directory.

<!— index.htm —>
<!doctype html>
<html>
<head>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Hello World</h1>
</body>
</html>
/* style.css */
body {
  background: #efefef;
}
# config.ru
map '/' do
  run Rack::Directory.new '.'
end

Now when you go to http://localhost:9292/index.htm all the static files will be served with the proper content types! Even better we can use Rack::Static that gives us a lot more power with its options. Let’s say this is our directory structure:

$ tree
.
├── config.ru
├── css
│   └── style.css
└── index.htm

Then our config.ru can look like this:

class RackApp
  def call(env)
    [
      200,
      {
        'Content-Type'  => 'text/html',
      },
      File.open('index.htm', File::RDONLY) # responds to each
    ]
  end
end

use Rack::Static, urls: ["/css"], root: '.'
run RackApp.new

Obviously the CSS stylesheet URL in index.htm will also need to be changed from style.css to /css/style.css.

Middlewares

You’d already be familiar with the concept of middleware from the Node.js/Ruby/Python world. If not then they’re basically layers that intercept the incoming request and can do something with it. Either they can change the response that is sent back or even prevent the propagation of the request down the middleware stack totally.

# config.ru
class RackApp
  def call(env)
    [ 200, {"Content-Type" => "application/json"}, ['{"response": "OK"}'] ]
  end
end

class Middleware
  def initialize(app)
    @app = app
  end

  def call(env)
    @status, @headers, @body = @app.call(env)

    puts 'middleware'
    [ @status, @headers, @body ]
  end
end

use Middleware
run RackApp.new # ruby object that responds to call()

First thing to notice is the app object that’s passed to the middleware’s initialize method. It’s the object of the next middleware whose call() method has to be executed, since middleware are also rack apps.

So when a request comes in, it goes down the middleware chain. First the Middleware’s call() method is called and then the RackApp#call() is fired. Although note when the server is booted, the rack app initialization happens up the chain, RackApp -> Middleware, whereas the request is passed down the chain, Middleware -> RackApp. So if we had 3 middleware, then it’d be like RackApp -> M1 -> M2 -> M3 (initialization) and M3 -> M2 -> M1 -> RackApp (request passing).

Using is a middleware is as simple as calling the use method and passing the middleware name to it. Finally the rack app required to be executed is run with the run method.

Inbuilt Libs and Middlewares

There are certain inbuilt libraries in Rack that can do useful things like query parsing for instance.

# config.ru
class RackApp
  def call(env)
    #p env['QUERY_STRING']

    req = Rack::Request.new(env)
    p req.post?
    p req.get?
    p req.params['hello']

    [ 200, {"Content-Type" => "application/json"}, ['{"response": "OK"}'] ]
  end
end

run RackApp.new

Now if you go to http://localhost:9292/?hello=world then the output of those method calls will be as follows:

p req.post? # false
p req.get? # true
p req.params[‘hello’] # ‘world'

Similarly there are some inbuilt middlewares like Rack::CommonLogger which are also super useful when building web apps.

# config.ru
class RackApp
  def call(env)
    [ 200, {"Content-Type" => "application/json"}, ['{"response": "OK"}'] ]
  end
end

file = File.new("access.log", 'a+')

use Rack::CommonLogger, file # additional arguments to middleware’s initialise()
run RackApp.new

Rack::CommonLogger‘s new method accepts an argument which should be an object that responds to the write method. We just created a file object and passed it to the middleware. Now whenever a request comes in, it’ll be logged to a file in the same directory called access.log.

It’s interesting how such a simple concept implemented in such a minimal way can power large frameworks like Rails which in turn ends up powering large apps!

References:

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

Leave a Reply

Your email address will not be published. Required fields are marked *