Rack Explained For Ruby Developers

What is happening behind the scenes of every Rails, Sinatra, and other Ruby web frameworks?

The answer is Rack, the key component that makes this possible.

But what is Rack exactly?

Rack is a layer between the framework (Rails) & the application server (Puma).

rack middleware

It’s the glue that allows them to communicate.

Why Do We Use Rack?

We use Rack because that allows different frameworks & servers to be interchangeable.

They become components that you can plug-in.

This means you can use Puma with Rails, Sinatra & any other Rack-compatible framework. It doesn’t matter what framework or server you are using if they implement the Rack interface.

With Rack, every component does its job & everyone is happy!

What is Rack Middleware?

Rack sits in the middle of every web request & response.

As a result, it can act as a guardian, by denying access to unwanted requests, or it can act as a historian, by keeping track of slow responses.

That’s what Rack middleware is!

Small Ruby programs that get called as part of the request-response cycle & get a chance to do something with it.

What kind of things is this used for?

  • Logging
  • Sessions
  • Profiling (find out how long a request takes to complete)
  • Caching
  • Security (deny requests based on IP address, or limit # of request)
  • Serving static files (css, js, png…)

These are pretty useful & Rails makes good use of middleware to implement some of its functionality.

You can see a list of middleware with rake middleware inside a Rails project.

Now, this Rack interface I mentioned earlier.

What does it look like?

Let me show you with an example…

How to Write Your Own Rack Application

You can learn how Rack works by writing your own application.

Let’s do this!

A Rack application is a class with one method: call.

It looks like this:

require 'rack'

handler = Rack::Handler::Thin

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

handler.run RackApp.new

This code will start a server on port 8080 (try it!).

What is this array being returned?

  • The HTTP status code (200)
  • The HTTP headers (“Content-Type”)
  • The contents (“Hello from Rack”)

If you want to access the request details you can use the env argument.

Like this:

req = Rack::Request.new(env)

These methods are available:

  • path_info (/articles/1)
  • ip (of user)
  • user_agent (Chrome, Firefox, Safari…)
  • request_method (get / post)
  • body (contents)
  • media_type (plain, json, html)

You can use this information to build your Rack application.

For example, we can deny access to our content if the IP address is 5.5.5.5.

Here’s the code:

require 'rack'

handler = Rack::Handler::Thin

class RackApp
  def call(env)
    req = Rack::Request.new(env)

    if req.ip == "5.5.5.5"
      [403, {}, ""]
    else
      [200, {"Content-Type" => "text/plain"}, "Hello from Rack"]
    end
  end
end

handler.run RackApp.new

You can change the address to 127.0.0.1 if you want to see the effect.

If that doesn’t work try ::1, the IPv6 version of localhost.

How to Write & Use Rack Middleware

Now:

How do you chain the application & middleware so they work together?

Using Rack::Builder.

Best way to understand is with an example.

Here’s our Rack app:

require 'rack'

handler = Rack::Handler::Thin

class RackApp
  def call(env)
    req = Rack::Request.new(env)

    [200, {"Content-Type" => "text/plain"}, "Hello from Rack - #{req.ip}"]
  end
end

This is the middleware:

class FilterLocalHost
  def initialize(app)
    @app = app
  end

  def call(env)
    req = Rack::Request.new(env)

    if req.ip == "127.0.0.1" || req.ip == "::1"
      [403, {}, ""]
    else
      @app.call(env)
    end
  end
end

This is how we combine them together:

app =
Rack::Builder.new do |builder|
  builder.use FilterLocalHost
  builder.run RackApp.new
end

handler.run app

In this example we have two Rack applications:

  • One for the IP check (FilterLocalHost)
  • One for the application itself to deliver the content (HTML, JSON, etc)

Notice that @app.call(env), that’s what makes FilterLocalHost a middleware.

One of two things can happen:

  • We return a response, which stops the middleware chain
  • We pass the request along with @app.call(env) to the next middleware, or the app itself

Inside any part of your Rack app, including middleware, you can change the response.

Example:

class UpcaseAll
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, response = @app.call(env)

    response.upcase!

    [status, headers, response]
  end
end

That’s exactly how Rack works 🙂

Summary

You’ve learned about Rack, the interface that is driving the interaction between Ruby web frameworks & servers. You’ve also learned how to write your own Rack application to understand how it works.

If you have any questions or feedback feel free to leave a comment below.

Thanks for reading!

4 comments
mikeL says last year

Hi Jesus. (sounds kinda funny no?..). Anyway, i don’t know if it’s just me but when i tried to run the second line of code
handler == Rack::Handler::Thin

i got an error message

LoadError: cannot load such file -- thin
from C:/Ruby22-x64/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'

i tried using both pry and irb and i think i got the same message.
i’ll still go and do some rubydocs research on Rack but i just thought i’d mention it….

    Jesus Castello says last year

    Hi, MikeL.

    Jesus is my real name, it’s common here in Spain 🙂

    To fix your error you need to install the Thin gem. A gem install thin should be enough.

    Thanks for you comment!

Rod says last year

This was an excellent explanation. Thanks!

    Jesus Castello says last year

    Thanks for your comment Rod 🙂

Comments are closed