RUBY

Rack Deep Dive – Part 1


About 7 months ago, I started teaching ruby and rails at Andela. One of the biggest challenge in teaching rails is explaining all of the “magic” that Rails uses to do its job . The most effective way to really understand how things work in rails is to rebuild it from scratch. Below is a quote by Chad Fowler.

The magic thing about Rails is when I looked at it the first time, I knew how it worked because I already knew Ruby really well.

I saw all the metaprogramming tricks; they were almost transparent, looking at it. But I didn’t know you could put it together that way and create something so expressive and succinct.

This is my attempt at building an MVC framework similar to rails to teach my fellows how rails work?

I broke down this series into multiple posts which I will be posting gradually. The posts are

  • Part 1 – Rack Deep Dive

  • Part 2 – Set up a Basic  Framework

  • Part 3 – Autoloading and Utility Methods

  • Part 4 – Better Routing

  • Part 5 – Render, Redirect & Before_Action Methods in Controllers

  • Part 6 – Extract Complexities out of View with Helpers

  • Part 7 – Reduce Rework with Generators

  • Part 8 – Set up ORM for Read/Write to Database 

  • Part 9 – Generate Database from Schema

  • Part 10 – Set up Migrations

For this series of posts, I’m going to assume that you have a general understanding of HTTP (for example: 2xx status code means success) and Ruby, though you may never have built a web framework before. :smile:

To understand Rails and even the more basic Sinatra, I think you need to go deeper and start with Rack.

What is Rack

Did you know that rails and sinatra are rack apps, so also is our future MVC framework. So what exactly is rack and how does it work.

In a sentence, Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks. It is a Ruby package that provides an easy-to-use interface to the Ruby Net::HTTP library.

All major Ruby web servers (Puma, WEBrick, Unicorn, etc) understand the Rack protocol, so if our app conforms to the Rack application specification, we can use those servers with it for free.

Since our MVC framework will be a rack app, understanding rack and it’s specification is necessary before we can proceed towards building our framework.

Rack Specification

From rack website(http://rack.github.io)

To use Rack, provide an “app”: an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements:

  • The HTTP response code
  • A Hash of headers
  • The response body, which must respond to each

You basically provide an object(not a class) that responds to call, passing in another object when the call method is called which will return another object. It is all ruby.

To fully understand rack and it’s specification, let’s build a rack app.

A Tiny Rack App

We are going to build a rack app with 3 lines of code. Create a file called tiny_rack_app.rb and add the following content to it.

In the first line, we required rack. In the second line, we created an object that responds to call. Remember that a proc has a call method and it executes the block passed to it during initialization. Notice also that the block passed to the proc accepts env as an argument and returns a rack compatible response(from specification).

The third line is used to boot our ruby server. You will notice this line:

Rack uses handlers to run Rack applications. Each Ruby webserver has its own handler, but I chose the WEBrick handler for this example, because WEBrick is installed by default with Ruby. You can use other handlers like Thin which is installed by default or you can use web servers like  Puma and Unicorn by installing and requiring the necessary gems. Run it using:

and navigate to localhost:9292. You will see I respond to all request in your browser.

We can also use the rackup command line tool(with config.ru file) and avoid specifying details like port and server until runtime.

In this case, I am using a lambda instead of a Proc. Run it using

and navigate to localhost:9292. You will see I respond to all GET request in your browser.

Another Rack App

Here is a rack app that is created from a custom class that responds to call method and that uses puma handler. Remember to install puma gem using gem install puma before using this handler.

Paste the above code in a file called another_rack_app.rb and run it using

Some interesting things are happening in the call method. It accept’s an environment hash as an argument(like any other rack app), then gets the verb and path from the env hash. In line 8, we set the response content-type header to be text/html and constructed a html response body in line 9. The final content returned in line 10 is a rack compatible response.

The final content returned in line 10 is a rack compatible response.

Environment Hash

Our Rack server object takes in an environment hash. What’s contained in that hash? Here are a few of the more interesting parts:

  • REQUEST_METHOD: The HTTP verb of the request. This is required.
  • PATH_INFO: The request URL path, relative to the root of the application.
  • QUERY_STRING: Anything that followed ? in the request URL string.
  • SERVER_NAME and SERVER_PORT: The server’s address and port.
  • rack.version: The rack version in use.
  • rack.url_scheme: is it http or https?
  • rack.input: an IO-like object that contains the raw HTTP POST data.
  • rack.errors: an object that response to puts, write, and flush.
  • rack.session: A key value store for storing request session data.
  • rack.logger: An object that can log interfaces. It should implement info, debug, warn,error, and fatal methods.

You can wrap the environment hash in a Rack::Request object to work with the environment hash in a more object oriented way. eg.

Accessing env directly quickly becomes tedious in any Rack project, more complex than AnotherRackApp. You should use Rack::Request

Middleware

Middleware gives you a way to compose Rack applications together.

In the real world, your rack app won’t work in isolation. More often you want to process the request or response before it hits your final rack app. Middleware

Middlewares are other Rack applications that comes between our final app and the HTTP request. Middleware is Rack’s true strength.

For example, if you have a Rails app lying around (chances are, if you’re a Ruby developer, that you do), you can cd into the app and run the command rake middleware to see what middleware Rails is using:

Below is an example middleware.

The difference between a middleware and your final rack app is that it’s initialized with an app, which is the “final” Rack app that it can pass the request on to.

The middleware above updates our request method(verb) with the value of method query param if it is set. So we can we can make a get request and change it to a post request like this: localhost:9292?method=post.

You can combine your middleware with your rack app using Rack::Builder.

For example, you can combine the MethodOverrideMiddleware middleware with the AnotherRackApp rack app above using

Try putting  the middleware code and the builder code in another_rack_app.rb file and remove the following lines.

Run it with

You are not limited to one middleware. You can stack as many middleware as possible as seen in rails.

With this, we are done with the discussion on rack.

In the next post, we will start building out the MVC Framework.

If you have any questions or observations, please drop your thoughts in the comment section below :smile:

Ikem Okonkwo

About Ikem Okonkwo

Ruby Evangelist, .NET Advocate. Trainer at @andela. Passionate about education and lifelong learning. Loves good food and soccer.