In Ruby on Rack #1 – Hello Rack! we used rackup to make port/server configurable. And rackup’s config file looked like :
1 2 |
# config.ru run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "Hello Rack!"]} |
Under the hood, rackup converts your config script to an instance of Rack::Builder.
What is Rack::Builder ?
Rack::Builder implements a small DSL to iteratively construct Rack applications.
Rack::Builder is the thing that glues various Rack middlewares and applications together and convert them into a single entity/rack application. A good analogy is comparing Rack::Builder object with a stack, where at the very bottom is your actual rack application and all middlewares on top of it, and the whole stack itself is a rack application too.
Let’s say our rack application is called infinity :
1 2 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} Rack::Handler::Mongrel.run infinity, :Port => 9292 |
All infinity does is send the env hash inspect string back to the browser.
Now, there are three important Rack::Builder instance methods that you should care about :
1. Rack::Builder#run
Rack::Builder#run specifies the actual rack application you’re wrapping with Rack::Builder.
Converting infinity to use Rack::Builder:
1 2 3 4 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new builder.run infinity Rack::Handler::Mongrel.run builder, :Port => 9292 |
Or you can follow the community convention and use the block form of Rack::Builder :
1 2 3 4 5 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new do run infinity end Rack::Handler::Mongrel.run builder, :Port => 9292 |
Here Rack::Builder#initialize accepts a block argument, which is evaluated within the context of newly created instance using instance_eval.
2. Rack::Builder#use
Rack::Builder#use adds a middleware to the rack application stack created by Rack::Builder. If the term “middleware” confuses you, don’t worry. Hopefully my next post will clean the air. Stick to the before/after/around filter analogy for now.
Rack has many useful middlewares and one of them is Rack::CommonLogger, which logs a single line to the supplied log file in the Apache common log format.
So if we’re to add Rack::CommonLogger to infinity :
1 2 3 4 5 6 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new do use Rack::CommonLogger run infinity end Rack::Handler::Mongrel.run builder, :Port => 9292 |
Line of interest is of course use Rack::CommonLogger. As we didn’t supply Rack::CommonLogger with an explicit logger, by default it’ll log to env[“rack.errors”]. Hence you’ll see logging strings in the console for every request.
3. Rack::Builder#map
Rack::Builder#map mounts a stack of rack application/middlewares the specified path or URI and all the children paths under it.
Let’s say you want to show “infinity 0.1” for all the paths under /version ( i.e. /version, /version/whatever BUT NOT /versionsomething ) , you might want to do something like :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require 'rubygems' require 'rack' infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new do use Rack::CommonLogger run infinity map '/version' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] } end end Rack::Handler::Mongrel.run builder, :Port => 9292 |
But that’s not going to work. Rack::Builder#map also encapsulates a scope within the builder. And one scope can just have one Rack::Builder#run method. In the example above, we have run infinity at the top level global scope and map ’/version’ has it’s own run too. Hence the conflict.
To fix this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new do use Rack::CommonLogger map '/' do run infinity end map '/version' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] } end end Rack::Handler::Mongrel.run builder, :Port => 9292 |
Now if you go to http://localhost:9292/version or http://localhost:9292/version/1 or even http://localhost:9292/version/whatever/doesnt/matter, you’ll see “infinity 0.1” and for all the URIs not starting with /version – http://localhost:9292 – you’ll see the env hash inspect string!
Please note that :
- /versionsomething WILL NOT show the version, but will display the env inspect.
- When you have multiple map blocks, URIs are tried from longest length to shortest length.
Nesting map blocks
Let’s say you feel like adding information about last version. So to show “infinity beta 0.0” at /version/last:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new do use Rack::CommonLogger map '/' do run infinity end map '/version' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] } end map '/version/last' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] } end end Rack::Handler::Mongrel.run builder, :Port => 9292 |
Above code will work perfectly as expected. You’ll see “infinity beta 0.0” at http://localhost:9292/version/last and “infinity 0.1” at http://localhost:9292/version.
But a better way (IMHO) to write the same code is by nesting map blocks :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} builder = Rack::Builder.new do use Rack::CommonLogger map '/' do run infinity end map '/version' do map '/' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] } end map '/last' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] } end end end Rack::Handler::Mongrel.run builder, :Port => 9292 |
This works perfect. When you nest map blocks, you’ll need to specify URI relative to the enclosing mapping block, as you can clearly see in the example above.
Rack::Builder -> rackup
As I mentioned above, rackup converts the supplied rack config file to an instance of Rack::Builder. This is how is happens under the hood ( just so you get an idea ) :
1 2 |
config_file = File.read(config) rack_application = eval("Rack::Builder.new { #{config_file} }") |
And then rackup supplies rack_application to the respective webserver :
server.run rack_application, options |
Very straight forward! In short, rack config files are evaluated within the context of a Rack::Builder object. So if we convert infinity to a rack config file which rackup can understand :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# infinity.ru infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]} use Rack::CommonLogger map '/' do run infinity end map '/version' do map '/' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] } end map '/last' do run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] } end end |
And now run it :
$ rackup infinity.ru





there’s a typo there: > $ rackup inifnity.ru $ rackup infinity.ru
@kaineer : Fixed, thanks!
So the “use” keyword (“use Rack::CommonLogger”) is a form of dependency injection?
Awesome articles. This is just what I’ve been looking for on rack to start digging in and poking around. Thanks for putting them up.
@Adam Kinda. I hope my next article will help.
@Jeremy Glad to help!
I was just looking for an article exactly like this one. Thanks for posting this, really helpful.
@Pratik – thanks for this writeup.
In a Sinatra app, how would you configure Rack::CommonLogger to send output to a log file rather than stdout ??
Rack is awesome. I actually just started playing with it a few days ago and then stumbled on your two (very recent) articles. Nice timing!
I’m thinking I’ll use Rack along with Sequel for a project (as well as some other new stuff that I haven’t used yet) I’m working on.
Some other cool Rack-ish stuff to look at if you haven’t already: Rack::Cache, mod_rack/mod_rails/Phusion Passenger.
I just started getting this error when running your #map example.
Thu Nov 20 15:36:15 -0800 2008: Read error: #<nomethoderror:><array:0x7f899f4b8ab8>> /usr/lib64/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/commonlogger.rb:20:in `_call’ /usr/lib64/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/commonlogger.rb:13:in `call’ /usr/lib64/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/builder.rb:53:in `call’
Damn good articles, Pratik. Explaining the value of Rack has always been difficult to me, even as someone who has developed a framework built on Rack. Middleware is one of the hardest, simplest concepts to really drive home.
Some thoughts:
I never write Rack::Builder manually, I almost always put my scripts in rackup config files or as class definitions as proper middleware.
Also, make sure you check out Merb’s middleware definitions, will probably find something useful for your next article.
Thin as an excellent implementation for creating a CLI interface in Thin::Runner. I used something very similar for Halcyon’s CLI util. It’s much cleaner (though a bit more complex) than looking at rackup for inspiration.
Keep up the good work, looking forward to the next article!
Great post!
Rack is really a great tool. It is being used in many ruby web frameworks. I know that Ruby Waves was built on it.
Thanks
@andy – I’ve just been trying to work out how to get Sinatra to use CommonLogger too, and I’ve come up with this (seems to work, but I’ve not really hit it much yet):
Since Rack provides access to Session how can I implement Single Signon for two Rails apps running on Passenger?