Concisely about Rack applications


2012-08-23 · 6 min read

Rack is a modular server interface for Ruby web applications. It unifies the API for web servers, web frameworks and middlewares. This post is a concise introduction to building basic Rack applications along with some useful tricks.

Simplest Rack Application

Let's start with a simplest possible Rack application.

run lambda {|env| [200, {'Content-Type'  => 'text/plain'}, ["Hello, Zaiste!"]]}

We define here a lambda expression that returns three elements: a numeric status, headers as a hash and (optional) body (as string for Ruby 1.8 and as array for Ruby 1.9). A Rack application can be defined as an object which responds to #call method. This method takes one argument env and it returns three mentioned elements. As lambdas also respond to #call method, the code above represents a Rack application and it will run without any issue.

Running Rack Apps

We can run a Rack application using rackup command along with a special configuration file. By default rackup looks for a configuration file named config.ru, if you prefer to use a different name, you have to specify it as rackup command invocation parameter.

Let's run our application:

λ rackup &
>> Thin web server (v1.5.0 codename Knife)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:9292, CTRL+C to stop

λ curl http://localhost:9292
127.0.0.1 - - [01/Jan/1979 19:10:44] "GET / HTTP/1.1" 200 - 0.0008
Hello, Zaiste!

Rack Apps as Ruby Objects

We can also write a Rack app using plain Ruby objects.

class RackApp
  def self.call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello, Zaiste!"]]
  end
end

Let's save this code as lib/rack_app.rb and put the following content into config.ru:

require 'rack_app'
run RackApp

To run it, we must add -Ilib parameter to rackup command.

λ rackup -Ilib &

Alternatively, we can run a Rack application just with ruby command, using either Rack::Server or Rack::Handler. Simply put one of following lines at the end of rack_app.rb file

Rack::Server.start app: RackApp
Rack::Handler::Thin.run RackApp

and run it as

λ ruby ./lib/rack_app.rb
>> Thin web server (v1.5.0 codename Knife)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:9292, CTRL+C to stop

Handling Requests

Information carried by requests, come to Rack application through env parameter of #call method. This gist shows the structure and possible content of this parameter.

For example, if we want to submit some information through URL (so-called GET data: the part of URL after ? sign), it will be stored in env['QUERY_STRING'].

curl http://localhost:9292/?foo=bar&baaz=none
{… "QUERY_STRING"=>"foo=bar&baaz=none", … }

Another example, an information from REQUEST_PATH could be used to implement a routing mechanism, i.e. triggering a specific action based on the local path of the requested resource from the request line.

Abstracting Requests and Responses

So far we've seen how to handle request parameters by hand and build responses from scratch. Rack::Request and Rack::Response provide convenient abstractions around these two concepts. The former class helps to handle incoming information, wrapping env hash and the latter makes it easier to generate response triplets.

Let's wrap a env parameter with Rack::Request:

req = Rack::Request.new env
req.params # the union of GET and POST data
req.params['foo'] # specific user-data
req.post?   # a POST request ?
req.xhr?   # an AJAX request ?
req.params['baaz'] =  'something'

It is important to note that modifying Rack::Request instance also modifies underlying env hash.

Creating a response is similar, we can easily set headers, cookies or define a response status.

resp = Rack::Response.new
resp.write 'Hello, Zaiste!'
resp['X-Custom-Header'] = 'foo'
resp.set_cookie 'foo', 'bar'
resp.status = 200
resp.finish

Let's put it all together as a whole Rack application

class FancyRackApp
  def self.call(env)
    req = Rack::Request.new(env)
    case req.path
    when "/"
      Rack::Response.new("Hello, Zaiste!")
    when /^\/name\/(.*)/
      Rack::Response.new("Hello, #{$1}!")
    else
      Rack::Response.new("Not found", 404)
    end
  end
end

And below results:

λ curl http://localhost:9292/
127.0.0.1 - - [01/Jan/1979 19:10:44] "GET / HTTP/1.1" 200 - 0.0008
Hello, Zaiste!
λ curl http://localhost:9292/name/John
127.0.0.1 - - [01/Jan/1979 19:10:44] "GET / HTTP/1.1" 200 - 0.0008
Hello, John!
λ curl http://localhost:9292/crazy
127.0.0.1 - - [01/Jan/1979 19:10:44] "GET / HTTP/1.1" 404 - 0.0008
Not found

Class vs Object

If you happen to instantiate a Rack object inside config.ru, it will be reused as long as Rack application runs. It means that the content of instance variables will be carried between requests if not set otherwise. It is a better idea to always define #call as a class method, i.e. pass in the class instead of an object inside rackup configuration file.

Cascading Rack Apps

Rack::Cascade provides a way to combine Rack applications as a sequence. It takes an array of Rack applications as an argument. When a new request arrives, it will try to use the first Rack app in the array, if it gets a 404 response it will move to the next one. Let's consider the following example:

require "rack_app"
run Rack::Cascade.new([Rack::File.new("public"), FancyRackApp])

The first app in our array is Rack::File, which serves static files from the directory provided as an argument. If there is a request for a file from public directory, Rack::File will try to look for it. If not found, Rack::Cascade will move execution to next application from the list.

Rack Middleware

Rack middleware provides a way to implement a chained process execution for web applications. It's an implementation of the pipeline design pattern. It acts « in the middle » between the client and the server processing requests before they reach the server and responses before they are returned to the client. Middlewares can be used for various purposes such as managing user sessions, authentication or configuring static files access etc.

A Rack middleware has similar structure to Rack application, i.e. it responds to #call. Unlike Rack application, Rack middleware has an initializer that takes another Rack application or middleware as a parameter. In other words we can « wrap » a simple Rack application with a middleware, and then again, we can wrap the result with another middleware, and so on. As Rack middlewares have access to a passed in application, they can perform actions before or after they are passed to another Rack application.

The use keyword are used to define middlewares to instantiate, while by ‘run’ keyword designates a Rack application.

Middleware Stack

Rack::Builder helps creating a middleware stack. It wraps one Rack middleware around another and then around given Rack application. Each object is instantiated with the next one, following in the stack as a parameter, creating a final Rack application.

app = Rack::Builder.new do
  use Rack::Etag           # Add an ETag
  use Rack::Deflator      # Compress
  run FancyRackApp     # User-defined logic
end

The code inside rackup configuration file is wrapped around with a Rack::Builder instance.

Reloading

There is a handy Rack middleware which reloads the source of Rack application if it changed.

use Rack::Reloader, 0
run RackApp

The only problem is that it only reloads Ruby files. If you have dynamic templates, you can take a look at rerun gem

Authentication

Another useful middleware is Rack::Auth::Basic. It can be used to protect our applications with Basic HTTP authentication.

use Rack::Auth::Basic, "Restricted Area" do |username, password|
  [username, password] == ['admin', 'admin']
end

Conclusion

Rack will help you better understand how HTTP protocol works and how popular Ruby frameworks like Rails make use of it. It is a good introduction to general mechanisms used in web applications.