skip to content

In Ruby, what is a Rack application, and what exactly must its call(env) method receive and return?

level: juniorimportance: must knowfreq 62%

answer

  1. any object that responds to call
  2. one argument: the env Hash
  3. String keys, CGI-style plus rack.*
  4. three-element Array, not frozen
  5. Integer status, lower-case headers, each-able body

basics

~20 s

A Rack application is any Ruby object that responds to call(env). It receives the request as an env Hash and returns a non-frozen three-element Array: an Integer status, a Hash of lower-case headers, and a body that responds to each or call.

solid answer

~40 s

Rack is the interface between Ruby web servers and frameworks. An app is anything with a `call` method: a lambda, a class instance, or a whole Sinatra or Rails application. The server builds an `env` Hash with String keys - CGI-style entries such as `REQUEST_METHOD`, `PATH_INFO`, `QUERY_STRING` and `HTTP_*` for request headers, plus `rack.*` entries such as `rack.input` for the body and `rack.errors`. The app returns `[status, headers, body]`: an Integer of at least 100, an unfrozen Hash whose keys are lower-case Strings, and a body that responds to `each` (yielding Strings) or `call` (streaming). Since Rack 3 the Array must not be frozen either. A String is not a valid body, because it does not respond to `each`; wrap it as `["ok"]`.

code

ruby · 9 lines
ruby
# config.ru
class Hello
  def call(env)
    name = env["QUERY_STRING"][/name=(\w+)/, 1] || "world"
    [200, {"content-type" => "text/plain"}, ["Hello, #{name}\n"]]
  end
end

run Hello.new

go deeper

for a junior

Recall the one method, call(env), and the three elements it returns, then write a lambda app from memory with an Array body and a lower-case content-type header.

for a middle

Explain the env Hash: CGI keys, HTTP_ header keys, rack.input and rack.errors. Show why a String body or a String status fails and how Rack::Lint reports it.

for a senior

Tie the contract to production: frozen or shared response objects leaking between requests, bodies that must be closed to release resources, and running tests through Rack::Lint to catch contract drift.

for a principal

Frame Rack as the seam between server and framework: because everything above it speaks call(env), you can swap servers, insert cross-cutting middleware or mount apps side by side without touching application code.

## What Rack is **Rack** is a small contract, published as `SPEC.rdoc` in the `rack` gem, that lets any Ruby web server talk to any Ruby web framework. The server (Puma, for example) parses HTTP and turns each request into a Ruby Hash; the framework turns that Hash into a response. Because both sides speak the same contract, a Sinatra app, a Rails app and a ten-line lambda are interchangeable from the server's point of view. The SPEC defines a **Rack application** as "a Ruby object that responds to `call`". It takes exactly one argument, the **environment**, and returns an `Array` of exactly three elements. ```ruby # config.ru run ->(env) { [200, {"content-type" => "text/plain"}, ["Hello"]] } ``` That line is a complete, deployable web application. ## The request: the env Hash The environment must be an **unfrozen `Hash` with String keys** (Rack 3.2 made String keys a SPEC rule). Its keys fall into three groups: - **CGI variables** without a dot: `REQUEST_METHOD` ("GET", "POST"...), `SCRIPT_NAME` (where the app is mounted), `PATH_INFO` (the rest of the path), `QUERY_STRING` (may be empty but is always present), `SERVER_NAME`, `SERVER_PROTOCOL`, and `SERVER_PORT` for a non-standard port. - **Request headers** as `HTTP_*` keys: `Accept-Language` arrives as `HTTP_ACCEPT_LANGUAGE`. Two headers are special: the SPEC forbids `HTTP_CONTENT_TYPE` and `HTTP_CONTENT_LENGTH` and uses `CONTENT_TYPE` and `CONTENT_LENGTH` instead. - **Rack variables** prefixed `rack.`: `rack.url_scheme` (`http`, `https`, `ws`, `wss`), `rack.input` (the request body as an IO-like object, when present), `rack.errors` (an error stream with `puts`, `write`, `flush`), and optional ones such as `rack.session` and `rack.response_finished`. Servers and libraries may add their own keys, but those must contain a dot and should use a unique prefix; `rack.` is reserved for the SPEC and the classes shipped in the gem. ## The response: three elements | Element | Rack 3 rule | Typical value | |---|---|---| | status | an `Integer` >= 100 | `200` | | headers | an unfrozen `Hash`; String keys with **no uppercase letters**; values a String or an Array of Strings | `{"content-type" => "text/html"}` | | body | responds to `each` (enumerable body) or `call` (streaming body); may also respond to `to_ary`, `to_path`, `close` | `["<h1>Hi</h1>"]` | A few consequences are worth saying out loud in an interview: 1. The **body is not a String**. Ruby's `String` has `each_line` and `each_char` but no `each`, so `"ok"` is neither an enumerable nor a streaming body. Use `["ok"]`. 2. The **status is an Integer**. `"200"` was tolerated by older servers; Rack 3's SPEC and `Rack::Lint` reject it. 3. **Header names are lower-case** in Rack 3: `content-type`, not `Content-Type`. Multiple values for one header go in an Array, for example two cookies under `set-cookie`. 4. The **Array must not be frozen**, because middleware further out may replace an element in place, such as the body after buffering it. 5. If the body responds to `close`, the server must call it once it is done, so the app can release files or connections. ## Why the contract is this small Parts of the SPEC are adapted, as it says itself, from Python's PEP 333 (WSGI). Keeping the interface to one method and three return values means: - **Middleware** can wrap any app: an object that takes an app in `initialize`, receives `env` in `call`, calls the inner app and returns (or edits) its triple. - **Frameworks** only have to produce the triple. Sinatra's routing, templates and filters all end in a `[status, headers, body]` Array. - **Servers** are swappable: nothing in the app depends on which server built the Hash. - **Testing** needs no server at all: build an env Hash (for example with `Rack::MockRequest.env_for`) and call the app directly. ## Checking an app against the SPEC `Rack::Lint` wraps an app and raises `Rack::Lint::LintError` (a `RuntimeError` subclass) as soon as it detects that the request or response breaks the SPEC: a frozen response Array, a non-Integer status, `uppercase character in header name: Content-Type`, a body yielding non-Strings, or a body that is never closed. Running the test suite through `Rack::Lint` is the cheapest way to find contract bugs before a server does. ## Where this sits in a real stack In production the server calls the outermost object of a stack assembled in `config.ru`: middleware wrapping middleware wrapping the endpoint app. Every layer, including the framework itself, obeys the same `call(env)` contract, which is why a question about "what Rails or Sinatra sits on" always comes back to this one method.

  • Why can a plain lambda be a Rack application, and when would you use a class instead?
    Rack only requires an object that responds to `call` with one argument, and `Proc#call` satisfies that. A lambda is fine for a tiny endpoint or a test double. A class is better when the app needs configuration passed to `initialize`, helper methods, or a name that shows up in stack traces and logs.
  • Where does the request body live in the Rack env, and what can you call on it?
    Under `rack.input`, when the request has one. The SPEC requires it to respond to `gets`, `read` and `each`, opened in binary mode with `ASCII-8BIT` encoding, and it may be closed early. Rack 3 no longer requires it to be rewindable, so read it once or wrap the app with `Rack::RewindableInput::Middleware`.

saying these in an interview costs you the question

  • Returning a String such as "ok" as the Rack response body
  • Believing a Rack app must subclass a Rack base class
  • Writing header names as Content-Type in a Rack 3 response
  • Reading request headers from env by their HTTP name, like env["Accept"]
  • Returning a frozen constant Array as the Rack response