A tribute to Ruby Rack

2016, Mar 28    

“Rack is a beautiful thing” - that’s how Ryan Bates started his Railscast about Rack middleware in 2009 and I totally agree with him over 7 years later. Rack is indeed a beautiful thing and to be honest I’m happy I started my Ruby web development adventure from Rails 3.2. Why? Because it has already had Rack support! Of course, it’s not a perfect solution for every web app (is there any?!), but it is widely used thanks to its simplicity and power. By the way, I have recently started a new project based on Rails 5.0.0.beta1 and it still uses Rack. Do you want to know why Rack is so awesome? Go on!

The basics

Rack is like a good waiter. It makes a Ruby web developer life easier by taking care of the requests like a waiter cares about the dishes in a restaurant. Thanks to that you can spend more time on coding your application rather than configuring the server environment to handle the HTTP requests properly. The best one-sentence Rack description comes from its homepage:

"Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks."

It is really minimal. Everything you need to know is that your application has to respond to call method, which takes in the environment hash and returns 3 elements in an array: code of the HTTP response, hash with headers and the response body. The last element (response body) must respond to each method.

To setup a dead simple Rack app (working in the browser!) just make a sample.ru file:

# sample.ru file
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['Rack and Roll!']] }

Why do we use a Proc here? Because it answers to the call method. This Proc contains the elements mentioned before, so the next step is to run the following command from the same directory you have your monster app.

rackup sample.ru

Voila! Launch your browser and visit localhost:9292 to see the “Rack and Roll” now! Of course, you can specify the port by passing an optional parameter in the command line:

rackup sample.ru -p 5678

and your app is available on localhost:5678. If you think that rails server command can be replaced with rackup in your app, I have a news for you - you’re right! Isn’t it beautiful? :-)

Rack under the hood

Rack runs under the hood of most of the Ruby web frameworks, not only Rails. Sinatra, Padrino, Cuba or Hanami (previously Lotus) also use Rack. Just take a look on ActionDispatch code shipped with Rails:

# /home/ozim/.rvm/gems/ruby-2.1.1/gems/actionpack-4.1.1/lib/action_dispatch/http/response.rb
def rack_response(status, header)
  assign_default_content_type_and_charset!(header)
  handle_conditional_get!

  header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)

  if NO_CONTENT_CODES.include?(@status)
    header.delete CONTENT_TYPE
    [status, header, []]
  else
    [status, header, Rack::BodyProxy.new(self){}]
  end
end

it returns the array that Rack is waiting for, just like Sinatra:

# /home/ozim/.rvm/gems/ruby-2.1.1/gems/sinatra-1.4.6/lib/sinatra/base.rb
def finish
  result = body

  if drop_content_info?
    headers.delete "Content-Length"
    headers.delete "Content-Type"
  end

  if drop_body?
    close
    result = []
  end

  if calculate_content_length?
    headers["Content-Length"] = body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s
  end

  [status.to_i, headers, result]
end

Of course - you can build your Ruby web apps without using Rack, but the reason it’s so commonly used is that it follows a few simple rules no matter which webserver and framework you use. There’s no need to spend extra hours (or days? or weeks?) dealing with the Ruby Net::HTTP library to serve the responses or the requests up and down the stack. Rack does it for you. For free!

Rails routes are Rack endpoints

If you’re still reading this, it’s worth to mention that all the Rails routes are Rack endpoints. You can write every single route in your app like that:

get "/rack_and_roll", to: proc { |e| [200, {}, ["Rack & Roll!"]] }

Rails just use a shorthand syntax to make it easier for you to create the routes. Sinatra works quite similar with its mount method. It all ends up with a Proc or Lambda. Need an evidence? Try that in the Rails console:

YourAwesomeController.action(:index)

and you will get something like:

 => #<Proc:0x00000003c3f700@/home/ozim/.rvm/gems/ruby-2.2.3/gems/actionpack-4.1.9/lib/action_controller/metal.rb:231>

it’s ready to call!

Your own middleware

One of the most powerful “features” Rack provides is a possibility to jump in between the server and application. That allows you to manipulate with HTTP request/response before passing it further down/up the stack. Visit the root folder of one of your Rails apps and type

rake middleware

in the terminal. You’ll see all of the things being processed right between your app and the web server. You’re very likely using some of the Rails standards like ActionDispatch::Cookies or Rails::Rack::Logger and it’s perfectly fine. Now think which of your apps does not use e.g. flash messages and you can remove the ActionDispatch::Flash from your middleware. Your app will thank you for that!

Do you have an idea to customize your requests flow? Go for it!

Conclusion

Rack is great not only for that it makes our life easier when it comes to handling the HTTP requests and responses. I feel like Rack helped the Ruby web development community to grow by unifying the API between the servers and the frameworks. Thanks to that we not only have Rails, we have much more frameworks to explore. Thanks to that we don’t need to use the default Webrick in development (actually Puma is now the default for Rails5.0.0.beta1), we can use Thin, Unicorn, Puma or Passenger. It all still makes the community grow and help devs to experiment and choose better. Of course - it’s not only Rack merit, but I think Rack has made a solid contribution to the Ruby/Rails family.

Rack is beautiful! :-)

Further reading:

  1. Official Rack website
  2. Rails Guides: Rails on Rack
  3. Rack middleware RailsCast
  4. Rack on GitHub
  5. Understanding Rack apps and middleware (EngineYard blog)