Rack: what a Ruby web app is
call(env)
A Rack application is any object with one method: call(env) returns a status, a header hash and a body that responds to each. Sinatra, Rails and every Rack server speak this interface, which is why they fit together:
class Hello
def call(env)
[200, { "content-type" => "text/plain" }, ["hello #{env["PATH_INFO"]}"]]
end
end
env is a hash with the request: the method, the path, the query string, the headers and the input stream. The body is an array of strings, not one string, so a large response can be streamed in pieces.
config.ru
rackup reads config.ru, builds the application and starts a server. The file starts with the class from the section above and is Ruby; its two verbs are use for middleware and run for the app at the bottom of the stack:
class Timing
def initialize(app)
@app = app
end
def call(env)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, body = @app.call(env)
us = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1_000_000).round(1)
[status, headers.merge("x-runtime-us" => us.to_s), body]
end
end
use Timing
run Hello.new
use Timing wraps the app: a request enters the middleware, calls the app, and leaves with one extra header. Middleware can change the request, the response, or both, and the order of the use lines is the order of the layers.
Middleware
The interface for middleware is the same as for an app: an object with call(env) that holds the next one. That is the whole idea, and it is why a logging layer, a session layer and an auth layer can be composed without knowing about each other.
- Request side. Read or rewrite env before calling the next layer: a header, a path prefix, a session cookie.
- Response side. Change the status, add a header, wrap the body: timing, CORS, compression.
- Short circuit. Return without calling the next layer: a 401, a redirect, a cached response.
rackup and a request
gem install rack
1 gem installed
rackup -p 9294
[2026-11-05 00:28:49] INFO WEBrick 1.9.2 [2026-11-05 00:28:49] INFO ruby 3.4.12 (2026-10-23) [x86_64-linux-musl] [2026-11-05 00:28:49] INFO Rackup::Handler::WEBrick::Server#start: pid=624 port=9294
One request through the stack, read with the header the middleware added:
ruby -rnet/http -e 'r = Net::HTTP.get_response(URI("http://127.0.0.1:9294/hi")); puts r.code; puts r["x-runtime-us"]; puts r.body'
200 4.0 hello /hi
Four microseconds is the time inside the app, not the socket. A middleware that measured the round trip would sit above the server adapter instead; the position in the stack decides what is measured.
What to watch for
Rack 3 requires lower-case header names in the response hash. A mixed-case
key is ignored by some servers and passed through by others, which is a
bug that appears only in production.
The body must respond to each and is closed by the
server. A body that holds a file handle needs a
close method, or the descriptor leaks after the
response; Rack calls it when it exists.
Next: A web app with Sinatra
Article author: Arthur Isaev