Post

Rack: The Swiss Army Knife of Ruby Web Servers

Rack: The Swiss Army Knife of Ruby Web Servers

What is Rack?

Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. It serves as a standard interface between web servers and web applications, allowing developers to use different web servers and frameworks seamlessly.

Rack App

To run an app service with Rack, you can use the rackup command, which is a powerful tool for starting web services. Here’s how you can implement a simple web service:

1
2
3
4
5
class Hello
  def call(env)
    [200, {"Content-Type" => "text/html"}, ["Hello there!"]]
  end
end

Save this code in a file, let’s say config.ru, and then you can start the server with rackup.

Rack App Structure

Here’s a visual representation of how the components interact with each other:

stateDiagram
    direction TB
    config.ru --> Rackup
    Rackup --> Rack
    Rackup --> Webrick
    Rackup --> Puma

    Rack --> Rails
    Rack --> Sinatra
    Rack --> Hanami

Data Flow

The data flow in a Rack application is straightforward:

stateDiagram
    direction LR
    [*] --> Request
    Request --> Rackup
    state Rackup {
      direction LR
      Webrick --> Rack
      Rack --> App
    }

Call Sequence

The sequence of a request handling in a Rack application:

sequenceDiagram
    box green HTTP Server
    participant WH as WEBrick::HTTPServer
    end
    participant RHW as Rackup::Handler::WEBrick
    box rgb(33,66,99) Rack Middlewares
    participant RC as Rack::ContentLength
    participant RCL as Rack::CommonLogger
    participant RL as Rack::Lint
    end
    box Blue App
    participant Hello
    end

    WH->>WH: run(socket)
    WH->>WH: service(request, response)
    WH->>RHW: service(request, response)
    RHW->>RC: call(env)
    RC->>RCL: call(env)
    RCL->>RL: call(env)
    RL->>Hello: call(env)

    Hello->>RL: [status, headers, body]
    RL->>RCL: [status, headers, body]
    RCL->>RC: [status, headers, body]
    RC->>RHW: [status, headers, body]
    RHW->>WH: [status, headers, body]

Boot up

The boot-up process involves several steps, starting from Rackup::Server.start to building the application stack using middleware.

Run the app

The Rack::Builder#run method is used to run the application:

1
Rack::Builder#run(app=#<Hello:0x000000011e0d49d0>, block=nil)

App Russian Doll

Rack applications can be nested with middleware, creating a “Russian doll” structure:

1
2
3
4
5
6
7
def build_app(app)
  middleware[options[:environment]].reverse_each do |middleware|
    klass, *args = middleware
    app = klass.new(app, *args)
  end
  app
end

Middleware

Middleware in Rack can be visualized as a series of layers:

stateDiagram-v2
    state ContentLength {
        state CommonLogger {
            state Lint {
                Hello
            }
        }
    }

Default Middleware

Rack comes with a set of default middleware that can be used in different environments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def default_middleware_by_environment
  m = Hash.new {|h, k| h[k] = []}
  m["deployment"] = [
    [Rack::ContentLength],
    logging_middleware,
    [Rack::TempfileReaper]
  ]
  m["development"] = [
    [Rack::ContentLength],
    logging_middleware,
    [Rack::ShowExceptions],
    [Rack::Lint],
    [Rack::TempfileReaper]
  ]
  m
end

Server

The server can be configured and started using the Rackup::Handler:

1
2
3
def server
  @_server ||= Handler.get(options[:server]) || Handler.default
end

And the WEBrick class is used to handle the server-side operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
module Rackup
  module Handler
    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
      def self.run(app, **options)
        ...
        @server = ::WEBrick::HTTPServer.new(options)
        @server.mount "/", Rackup::Handler::WEBrick, app
        yield @server if block_given?
        @server.start
      end
    end
  end
end

Conclusion

Rack, with its simple and flexible design, allows developers to build web applications in Ruby with ease. Whether you’re using Rails, Sinatra, or any other framework, Rack provides a consistent interface for handling requests and responses. The ability to chain middleware and the simplicity of setting up a web server make Rack an indispensable tool in the Ruby web developer’s toolbox.

This post is licensed under CC BY 4.0 by the author.