— originally on blog.arkency.com

Explaining Rack — desugaring Rack::Builder DSL

Yesterday I wrote a post highlighting Basic Auth and how can we protect Rack applications mounted in Rails with it. Today when discussing some ideas from this post with my colleague, our focus immediately shifted to Rack::Builder.

On one hand Rack interface is simple, fairly constrained and well described in spec. And ships with a linter to help your Rack apps and middleware pass this compliance.

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.

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello world!"]]
  end
end

On the other hand your first exposure to Rack is usually via config.ru in Rails application:

# This file is used by Rack-based servers to start the application.

require_relative "config/environment"

run Rails.application

Behind the scenes, this file is eventually passed to Rack::Builder. It is a convenient DSL to compose Rack application out of other Rack applications. In yesterday's blogpost we've seen it being used directly:

Rack::Builder.new do
  use Rack::Auth::Basic do |username, password|
    ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV.fetch("DEV_UI_USERNAME"))) &
      ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV.fetch("DEV_UI_PASSWORD")))
  end
  run Sidekiq::Web
end

Whether you use Rack::Builder directly or via rackup files, you're immediately associating Rack with the use and run DSL. And that triggered an honest question from my colleague — how does this DSL relate to that rather simple Rack interface?

De-sugaring Rack::Builder DSL

To add even more nuance, some Rack apps are called a middleware. What is a middleware? In simple words — it's a Rack application that wraps another Rack application. It may affect the input passed to the wrapped app. Or it may affect the output if it.

An example of a middleware that makes everything sound more dramatic:

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

  def call(env)
    status, headers, body = @app.call(env)
    [status, headers, body.map { |x| "#{x}111one!1" }]
  end
end

A composition of such middleware and our previous sample HelloWorld application with config.ru would look like this:

# config.ru

class HelloWorld
  # omitted for brevity
end

class Dramatize
  # omitted for brevity
end

use Dramatize
run HelloWorld.new

When executed, it would return very dramatic greeting:

$ bundle exec rackup config.ru
* Listening on http://127.0.0.1:9292
* Listening on http://[::1]:9292
Use Ctrl-C to stop

$ curl localhost:9292                                                    
Hello world!111one!1⏎ 

Now back to the question that started it all:

How does this DSL relate to that rather simple Rack interface?

The last example of composition via Rack::Builder can be rewritten to avoid some of the DSL:

# config.ru

run Dramatize.new(HelloWorld.new)

A single run is needed to tell a Ruby application server what is our Rack application that we'd like to run. The use of use is on the other hand just optional.

If this post got you curious on Rack, a fun way to learn more about it is to check the code of each middleware powering your Rails application:

$ bin/rails middleware
use Webpacker::DevServerProxy
use Honeybadger::Rack::UserInformer
use Honeybadger::Rack::UserFeedback
use Honeybadger::Rack::ErrorNotifier
use Rack::Cors
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Warden::Manager
use Rack::Deflater
use RailsEventStore::Middleware
run MyApp::Application.routes

Happy learning!