Advanced Rack
Almost exactly one year ago, I started teaching Metis, and Josh and I taught Sinatra to the students. A few of the students asked about how Sinatra works, and specifically how it’s built. We didn’t go too in-depth in class, but it piqued my curiousity and I started learning a lot more about Rack.
For this post, I’m going to assume that you have a general understanding of HTTP (for example: 2xx status code means success) and Ruby, though you may never have built a Rack app before. OK? Cool.
Let’s dive in to Rack.
What is Rack?
In a sentence, Rack specifies a standard interface for Ruby webservers.
All major Ruby web servers (Puma, WEBrick, Unicorn, etc) understand the Rack protocol, so if your app conforms to the Rack application specification you can use those servers with it for free.
Rack is a great foundation for web frameworks, like Rails and Sinatra. It gives you a Ruby-flavored interface to the HTTP request/response cycle. Since Rails is built on Rack, understanding Rack will help you write better Rails code and tests, including making fake remote services and catching JSON errors.
The Rack specification
Straight from the Rack City Department of Records, where the official Rack docs live:
A Rack application is a Ruby object (not a class) that responds to
call.
It takes exactly one argument, the environment, and returns an Array of exactly three values: The status, the headers, and the body.
As you can see, there’s very little overhead to making a Rack application. It’s an object that takes in a well-specified object and returns another well-specified object. It’s Just Ruby™.
Let’s build a Rack app and break it down.
Before going farther,
install the rack
gem,
which has some helpful utilities that we’ll use:
gem install rack
A minimal Rack app
Here’s a Rack app that responds with the type of request you send it:
# cool_rack_application.rb
require "rack"
class CoolRackApplication
def call(env)
http_verb = env["REQUEST_METHOD"]
status = 200
headers = {}
body = ["got #{http_verb} request\n"]
[status, headers, body]
end
end
# Run on localhost, port 9292
Rack::Handler::WEBrick.run(CoolRackApplication.new, Port: 9292)
As you can see, it’s Plain Old Ruby: CoolRackApplication
doesn’t inherit from
anything, and it doesn’t mix in any modules.
Try running that right now with ruby cool_rack_application.rb
,
then visit http://localhost:9292.
Neat, right?
Let’s go over that code, starting with the env
parameter.
The Rack environment
The env
variable that gets passed in to call
is the Rack environment,
and you can read about what’s in it in the Rack docs.
It has a lot of information about the request, like what HTTP verb was used, the
request path, the host, and more.
We’re grabbing env["REQUEST_METHOD"]
,
which is a string referring to the verb,
like "GET"
or "POST"
.
Then we respond to requests with a 200 response with body text
like “got POST request”.
The Rack specification says the body (the third element in the array)
must respond to #each
, so we wrap it in an array.
Rack::Request
An even easier way to use the Rack environment is
to wrap it in Rack::Request
, like so:
request = Rack::Request.new(env)
Rack::Request
also lets us replace direct env
access
like env["REQUEST_METHOD"]
with easy-to-read methods like request_method
.
Here are some quick comparisons of
direct env
access versus Rack::Request
:
# env["REQUEST_METHOD"]
request.request_method
# env["rack.request.query_hash"] + env["rack.input"]
request.params
# env["REQUEST_METHOD"] == "GET"
request.get?
Accessing env
directly quickly becomes tedious
in any Rack project.
You should use Rack::Request
.
Handlers
Let’s look again at this line:
Rack::Handler::WEBrick.run(CoolRackApplication.new, Port: 9292)
Rack uses handlers to run Rack applications. Each Ruby webserver has its own handler, but I chose the WEBrick handler for this example, because WEBrick is installed by default with Ruby.
If Puma is installed, we can use the Puma runner:
require "rack/handler/puma"
Rack::Handler::Puma.run(CoolRackApplication.new, Port: 9292)
Rack ships with a few built-in handlers, and other Ruby webservers provide their own, like Puma.
Middleware
CoolRackApplication
receives the HTTP response and responds directly. This is
actually uncommon in the Rack world - more often we want to process the request
or response before it hits our final application. The way we do that is with
middleware: other Rack applications that come between our final app and the
HTTP request. Middleware is Rack’s true strength.
Here’s some middleware that blocks every PATCH request:
class PatchBlockingMiddleware
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if request.patch?
[405, {}, ["PATCH requests not allowed!\n"]]
else
@app.call(env)
end
end
end
PatchBlockingMiddleware
(which, again, is another Rack app) has a call
method that acts
just like our call
method above - it takes in an env and returns a 3-element
array. The difference is that it’s initialized with an app
, which is the “final”
Rack app that it will pass the request on to.
In the PatchBlockingMiddleware
above, if the request is a PATCH request, it
immediately returns a 405 response with an error message. It doesn’t pass the
request on; it halts immediately and gets no farther. Otherwise, it passes
control on to the app.
Let’s look at Rack::Builder
and how it works to clear it up a little more.
Rack::Builder
In a sentence, Rack::Builder
is how you combine middleware and Rack apps.
Here’s how you’d combine PatchBlockingMiddleware
and CoolRackApplication
:
app = Rack::Builder.new do
use PatchBlockingMiddleware
run CoolRackApplication.new
end
Rack::Handler::WEBrick.run(app, Port: 9292)
Every incoming request goes through PatchBlockingMiddleware
first. If PatchBlockingMiddleware
passes it on with @app.call(env)
, then the request gets passed to
CoolRackApplication.new
.
Try putting the new Rack::Builder
code in cool_rack_application.rb
with the classes we’ve built
already and running it. Then watch it block requests:
# This PATCH request will be blocked
$ curl -X PATCH localhost:9292
# This GET request will not be blocked
$ curl -X GET localhost:9292
We can stack on as much middleware as we want:
app = Rack::Builder.new do
use PatchBlockingMiddleware
use OtherMiddleware
use Rack::Deflatermaus
run CoolRackApplication.new
end
# Run the app we built
Rack::Handler::WEBrick.run(app, Port: 9292)
Further Reading
Ruby Learning’s introduction to Rack is excellent.
If you’d like to see a truly silly Rack middleware that I built, check out rack-deflatermaus.