RubyGuides
Share this post!

Rack Explained For Ruby Developers

Would you like to know what goes on behind the scenes of Rails, Sinatra, and other Ruby web frameworks?

Then you’re in the right place.

Because today we are going to talk about Rack!

Rack is the key component that makes Ruby web development possible…

…but what is Rack exactly?

rack middleware

Rack is a layer between the framework & the application server (Thin, Puma, Unicorn…).

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 & other frameworks.

It doesn’t matter what framework or server you are using if they implement the Rack interface.

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

What is Rack Middleware?

Because Rack sits in the middle of every single request & response that your app produces it’s in a great position to be able to change or respond in some way to these requests.

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 like this:

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

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

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)

By using Rack::Builder we can say in what order they should run.

In this case, FilterLocalHost is the middleware & it will run first.

Inside this middleware we either return a response, which keeps the rest of the middleware chain from running, or we pass the request along with @app.call(env).

If you want to change the response instead of doing something with the request…

…first call @app.call(env) then you’ll get the response & you can change it however you want.

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 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!

Leave a Comment:

4 comments
mikeL says a couple of months ago

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….

Reply
    Jesus Castello says a couple of months ago

    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!

    Reply
Rod says last month

This was an excellent explanation. Thanks!

Reply
    Jesus Castello says last month

    Thanks for your comment Rod 🙂

    Reply
Add Your Reply