In this article we will discuss how we can sail with Rack in a real-world problem.
What Is Rack
Rack is a minimalistic Ruby Web server Interface. As a matter of fact, we can use Rack to build web applications if we follow its protocol. Notably, the protocol is straightforward. We need a Ruby object that responds to the call
method which returns a three-element Array:
# rack_hello_world.rb
require 'rack'
class HelloWorldApp
def self.call(env)
# 200 is the HTTP status code
# the second element is the response HTTP header hash
# finally the last element is the response body
['200', {'Content-Type' => 'text/html'}, ['A hello world rack app.']]
end
end
Rack::Handler::WEBrick.run HelloWorldApp
To try the above rack application:
$ gem install rack
$ mkdir rack_hello_world
$ cd rack_hello_world
Write the rack_hello_world.rb
file and execute:
$ ruby rack_hello_world.rb
$ curl -I http://localhost:8080
The Middleware
We can compose Rack applications together using middlewares. A middleware basically lets us wrap different inputs and outputs in order to integrate them into our problem-solving.
One common usage is in Rails. For example, Rails uses middlewares to wrap HTTP requests in a simple way.
Continuing from the previous example lets implement a middleware which will add some headers to our response:
# rack_hello_world.rb
# ...
class AddSomeHeaders
def initialize(app)
@app = app
end
def call(env)
@app.call(env).tap { |status, headers, body| headers['X-awesome'] = true }
end
end
app = Rack::Builder.new do
use AddSomeHeaders
run HelloWorldApp
end
Rack::Server.start app: app
The response headers:
$ curl -I http://localhost:8080
# => HTTP/1.1 200 OK
# => Content-Type: text/html
# => X-Awesome: true
# => Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21)
# => Date: Thu, 01 Jan 2018 21:29:53 GMT
# => Content-Length: 23
# => Connection: Keep-Alive
The Real World
For a real-world usage of a Rack middleware, we can open the Rails project.
# /actionpack/lib/action_dispatch/middleware/ssl.rb
# I simplified the call for the article's purpose
def call(env)
request = Request.new env
if request.ssl?
@app.call(env).tap do |status, headers, body|
set_hsts_header! headers
end
end
end
As a matter of fact, we can observe that the tap
is being used. This basically enables us to manipulate the app object(here the response) in a clean way at a later time. To put it differently, the logic is that @app.call(env)
will have to return before the tap
block gets executed.
The above is equivalent to:
def call(env)
request = Request.new env
if request.ssl?
res = @app.call(env)
res[1] = set_hsts_header! res[1]
res
end
end
It’s important to realize that if we want to place a middleware before the ActionDispatch::SSL
one we would have to tap(sic) into the object after the ActionDispatch::SSL
middleware is done.
Indeed, we can observe this functionality if we extend our previous example:
# rack_hello_world.rb
# ...
class EditSomeHeaders
def initialize(app)
@app = app
end
def call(env)
@app.call(env).tap do |status, headers, body|
headers.delete('X-awesome')
headers['X-double-awesome'] = 'true'
end
end
end
app = Rack::Builder.new do
use EditSomeHeaders
use AddSomeHeaders
run HelloWorldApp
end
Rack::Handler::WEBrick.run app
Notice how EditSomeHeaders
is being placed before AddSomeHeaders
in the middleware stack.
To clarify, execute:
$ curl -I http://localhost:8080
# => HTTP/1.1 200 OK
# => Content-Type: text/html
# => X-Double-Awesome: true # => This has changed
# => Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21)
# => Date: Thu, 01 Jan 2018 22:24:29 GMT
# => Content-Length: 23
# => Connection: Keep-Alive
Meanwhile, the full code example is available on github.
Takeaway
In this article, we’ve explored the basic concepts of Rack, built a Rack middleware and investigated how Rails utilizes
Rack to enforce the SSL policy.
All things considered, reading the code under the hood of a famous or well-engineered library or framework can teach us new methodologies and ways of solving problems. In essence, going through the Rails source code taught us how to use the tap
method to manipulate a Rack object that is being manipulated later in the stack.