Part 1: A Dive into ruby’s rack!

Being a beginner web developer, there is nothing more magical than Ruby on Rails. You get introduced to this awesome world of web development where everything works magically.
I never imagined creating an application would be this simple. You feel like God, or rather like Neo from the matrix.


However, once the dust settled, the curious side of me slowly started asking myself, what is this rails magic? What is a web framework actually doing?Have I become more of a Rails guy and less of a ruby person? How do I tell a layman how all these pieces fit together?
So I thought of tackling these questions one by one, I thought it’d be best to start from the basics.I then made a list of questions that needed to be answered, they were:

  • How do Rails take an HTTP request from the outside, process it and give back an HTTP response?
  • How does an application/web server like Unicorn, Puma, Webrick etc talk to rails and how do Rails talk back to these app servers?

On googling these questions(that’s how we learn right?), all the results that came up were somewhat related to ‘Rack’
All of us, even the beginners might have heard of Rack somewhere in our rails journey. In this post I plan to throw some light on this mysterious unicorn(not the server, the actual one) called rack.

Single worker unicorn :P

Concretely, I plan to answer the following questions:
 1. What is rack and why do we need it?
 2. What does it have to do with a rails application?
 3. What is Rack middleware?

Also as a part 2 of this post, I’ll try out building a small web application from scratch with the knowledge learned by answering these questions.

Sit tight as we embark on this rewarding journey!

What is rack and why do we need it?

Suppose you are running a simple blog application. When you hit something like blog_app/posts from your browser, your browser sends out an HTTP request which looks something like this:

GET /posts HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.medium.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

The web server running on your server listens for HTTP requests(Unicorn, Puma, Webrick etc).When it receives this HTTP request, it can’t just directly give this request to the Rails application, as rails has no idea what to do this mysterious looking HTTP request.(This is completely justified as an application need not be aware of how an HTTP request is processed, after all, it falls outside of the application’s jurisdiction). So how do we solve this problem and make the web server and the application talk to each other in a way that both of them are happy?
Many big bearded intelligent computer programmers sat in a closed room and came up with a solution called Rack. Rack is supposed to be the following:

  • Act as a middle man between the web application(rails here) and the web server. Process the incoming HTTP requests and handover the processed input to the rails application.
  • Once the request is processed by the application(ex. fetches all blogs from the database), get the results(here, an array of active record objects) back from the rails application and modify it so that it can be given out as a proper HTTP response by the web server back to the client.

Turns out if this is done, rack can act as an interface between any ruby based web application and web server, provided they follow some guidelines set by rack.(Which make them rack compliant)
So why do we need rack? Why not include all this inside the application?Two reasons:

1) It separates out the different stages of processing a request. This principle of separating different aspects of a software comes under the realm of Separation of concerns.(Go ahead and read about it), which is exactly what rack accomplishes.

2) Without rack, we wouldn’t have something generic, which can run across different web frameworks and web servers.

What does rack have to do with a rails application?

As mentioned before rack acts as an interface between the web server and your web application. Rack specifies a set of rules in the implementation of the web server and the web application, they are:

1) The web server will convert the incoming HTTP requests into a ruby hash called env .The contents of the env hash will look something like this:

{
"GATEWAY_INTERFACE" => "CGI/1.1",
"PATH_INFO" => "/posts",
"QUERY_STRING" => "",
"REMOTE_ADDR" => "::1",
"REMOTE_HOST" => "localhost",
"REQUEST_METHOD" => "GET",
"REQUEST_URI" => "http://localhost:3000/posts",
"SCRIPT_NAME" => "",
"SERVER_NAME" => "localhost",
"SERVER_PORT" => "3000",
"SERVER_PROTOCOL" => "HTTP/1.1",
"SERVER_SOFTWARE" => "WEBrick/1.3.1 (Ruby/2.0.0/2013-11-22)",
"HTTP_HOST" => "localhost:3000",
"HTTP_USER_AGENT" => "Mozilla/5.0",
"HTTP_ACCEPT" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"HTTP_ACCEPT_LANGUAGE" => "zh-tw,zh;q=0.8,en-us;q=0.5,en;q=0.3",
"HTTP_ACCEPT_ENCODING" => "gzip, deflate",
....
}

this env hash is then sent to the web application. The web application should implement a method called call which accepts this env hash.
With the contents in this env hash, the application is now ready to process the actual request(In rails this is done by ActionDispatch::Routing ). A sample implementation of the call method inside the application looks something like this:

class RackApp
def call(env)
#do some action according to the contents in the env hash
result = action(env["PATH_INFO"])
[200, { 'Content-Type' => 'text/html' }, result]
end
end

2) A rack compliant application should return the result as an array with exactly 3 elements to the web server. First is the HTTP status code, second is a hash containing the HTTP headers, lastly a ruby object which responds to .each method.

#Sample result from the application
[200, { 'Content-Type' => 'text/html' }, ["hello","world"]]

Any ruby web application or web server confirming to these rules becomes a rack compliant application or simply a rack application. So rails by itself is a rack application(meaning somewhere inside rails the call method has been implemented.).And all the common application servers like unicorn, puma, webrick are all rack compliant web servers, which is why you can easily switch from unicorn to puma without any major implementation change in your application.

What is rack middleware?

Rack middleware is a piece of functionality you want to add within the request response cycle.Say for example you want to measure how much time it took for your application to process the received request, you do something like this in the call method,

def call(env)
start_time = Time.now
# the request is processed by the application
result = some_action(env)
end_time = Time.now
process_time = end_time - start_time
puts process_time
[200, { 'Content-Type' => 'text/html' },result << process_time ]
end

Basically you measure the difference between the start_time and end_time to get the time it took for the application to process the request and then push this to the result array, So that you can see how much time it took for your application to process the request.
Another example would be caching, you can directly show a cached response if the same request comes again. 
Think of all these extra functionalities as layers which you can add to your application’s request response cycle, then rack middleware is nothing but a collection of these layers you add to your rack application.
You can see the list of rack middleware your rails application uses, by running rake middleware inside your rails application’s root directory.

Rails casts have an excellent episode on rack middleware,where he even shows how to create your own middleware(How cool is that?!) you can watch it from here.

That’s it people, I hope I’ve been able to give a relatively simple explanation of rack and middleware. Please pardon me for newbie mistakes.
Feel free to write down your thoughts/suggestions in the comment section.
Peace out!