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:
- http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html
- http://www.rubydoc.info/github/rack/rack/master/file/SPEC
- http://eftimov.net/rack-first-principles/
- http://southdesign.de/blog/rack.html