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).
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!
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
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….
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!
This was an excellent explanation. Thanks!
Thanks for your comment Rod 🙂