In a Rack app, what do Rack::Request and Rack::Response give you over the raw env Hash and the response Array?
answer
- thin wrappers, same env object
- params is GET merged with POST
- parsed values cached in rack.request.* keys
- Response.new(body, status, headers)
- finish returns the triple
basics
~20 sRack::Request wraps the env Hash with readers such as params, path_info, get?, cookies and ip, caching parsed values in env. Rack::Response collects status, headers and body with helpers such as write, set_cookie and redirect, and finish returns the SPEC triple.
solid answer
~40 s`Rack::Request.new(env)` does not copy anything: it reads the same `env` and caches parsed results back into it under `rack.request.*` keys, so several Request objects share the work. It offers `request_method` and `get?`/`post?`, `path_info`, `script_name`, `path`, `fullpath`, `url`, `params` (query params merged with form params, form values winning), `cookies`, `ip`, `xhr?` and `body` for `rack.input`. `Rack::Response.new(body = nil, status = 200, headers = {})` stores headers in a `Rack::Headers`, so keys are lower-cased; `write` appends chunks, `set_cookie` adds a `set-cookie` value, `redirect(url, 302)` sets status and location. `finish` returns `[status, headers, body]`, adding `content-length` when it knows the length and dropping entity headers for 204 and 304. `Request#[]` was removed in Rack 3.1: use `request.params["name"]`.
code
ruby · 14 linesrequire "rack"
app = lambda do |env|
req = Rack::Request.new(env)
res = Rack::Response.new
res.content_type = "text/plain"
res.write("id=#{req.params["id"]}")
res.set_cookie("seen", "1")
res.finish
end
res = Rack::MockRequest.new(app).post("/items?id=1", params: {"id" => "2"})
res.body # => "id=2"
res.get_header("content-length") # => "4"go deeper
Recall the everyday calls: Rack::Request#params, #path_info and #get?, and Rack::Response#write, #status= and #finish.
Explain that Request is a view over env with caches stored in env, how params merges GET and POST, and what finish adds to the triple.
Spot the traps in middleware: stale parse caches after rewriting env in Rack 3.2, removed APIs in upgraded code, and entity headers on 204 and 304 responses.
Decide how much a team builds on raw Rack objects versus a framework, knowing that shared env caches couple middleware to each other.
## Why wrappers exist The Rack contract is deliberately raw: a Hash in, a three-element Array out. Reading a form field from `env` by hand means reading `rack.input`, checking `CONTENT_TYPE`, and parsing URL-encoded or multipart data. `Rack::Request` and `Rack::Response` package those chores. Frameworks build on them, and they are the right tools inside small apps and middleware. ## Rack::Request: a view over env `Rack::Request.new(env)` stores the Hash and nothing else. Every reader goes back to `env`: - **Request line:** `request_method`, `get?`, `post?` and the other verb predicates, `scheme`, `path_info`, `script_name`, `path` (`script_name + path_info`), `fullpath` (with the query string), `url`, `query_string`. - **Parameters:** `GET` parses the query string, `POST` parses a URL-encoded or multipart body, and **`params` is `GET.merge(POST)`**, so a form field overrides a query parameter of the same name. `update_param` and `delete_param` change them in place. - **Headers and client:** `get_header("HTTP_ACCEPT")`, `content_type`, `cookies` (a Hash parsed from the Cookie header), `xhr?`, `ip`. - **Body and session:** `body` is `rack.input`; `session` reads `rack.session` when a session middleware set one. Parsed values are **cached in `env`** under keys such as `rack.request.query_hash`, `rack.request.form_hash` and `rack.request.cookie_hash`. Two consequences: 1. Creating `Rack::Request.new(env)` in several middleware is cheap: the body is parsed once and shared. 2. Since Rack 3.2 the cache is **not invalidated automatically**: if code rewrites `QUERY_STRING` after `GET` has run, `GET` keeps returning the old Hash. Removed APIs to recognise in old code: `request["name"]` (`Rack::Request#[]`, removed in 3.1) becomes `request.params["name"]`; `request.values_at(...)` (removed in 3.2) becomes `request.params.values_at(...)`. ## Rack::Response: building the triple `Rack::Response.new(body = nil, status = 200, headers = {})` accepts headers as a Hash and copies them into a **`Rack::Headers`**, which lower-cases keys, so `response["Content-Type"]` and `response["content-type"]` reach the same entry. The body argument decides buffering: | Body passed | Behaviour | |---|---| | `nil` | empty buffered body; add chunks with `write` | | a String | wrapped as `[string]`, length known | | anything else | used as-is, expected to follow the body SPEC | Useful methods: - `write(chunk)` appends to the buffer; `status=`, `content_type=`, `location=`. - `set_cookie(key, value)` and `delete_cookie(key)` add `set-cookie` values; repeated headers become Arrays through `add_header`. - `redirect(target, status = 302)` sets both status and `location`. - Predicates from `Rack::Response::Helpers`: `ok?`, `not_found?`, `redirect?`, `client_error?` and others. - **`finish`** returns `[status, headers, body]`. It sets `content-length` when the length is known and the response is not chunked, and for a status that carries no entity body (such as 204 or 304) it deletes `content-type` and `content-length` and returns an empty body. `to_a` is an alias. `Rack::Response[status, headers, body]` builds one from an existing triple. Passing a block to `finish` makes the response itself the body, and the block runs with the response when the server iterates it, so it can `write` more chunks as they are produced. ## A small endpoint with both ```ruby run do |env| req = Rack::Request.new(env) res = Rack::Response.new if req.get? && req.path_info == "/hello" res.content_type = "text/plain" res.write("Hello, #{req.params.fetch("name", "world")}") else res.status = 404 end res.finish end ``` ## Pitfalls worth naming - **Values are Strings.** `params["page"]` is `"2"`, not `2`; convert explicitly with `Integer()` and handle bad input. - **Nested names nest.** Query and form keys such as `user[name]=Ada` are expanded by `Rack::Utils.parse_nested_query` into `{"user" => {"name" => "Ada"}}`; Rack 3 no longer accepts the malformed `a[b[c]]` form as equivalent. - **Uploads are Hashes.** A multipart file field arrives as a Hash with `:filename`, `:type`, `:name`, `:tempfile` and `:head`; read the data from `:tempfile`. - **Returning the object.** An app must return `res.finish`, not `res`; `Rack::Response` has no `call` and is not itself the triple. - **Body reads.** `req.body` is the raw `rack.input` stream; after `POST` has parsed a form, it is at EOF and Rack 3 does not rewind it. ## Testing counterparts `Rack::MockRequest.new(app).get("/hello?name=Ada")` builds the env with `Rack::MockRequest.env_for`, calls the app and returns a `Rack::MockResponse`, a `Rack::Response` subclass with `status`, `body` and header readers. Passing `lint: true` wraps the app in `Rack::Lint` for that request.
- Why can two middleware each create Rack::Request.new(env) without parsing the body twice?`Rack::Request` keeps no data of its own. The first call to `POST` or `GET` stores the parsed Hash in `env` under `rack.request.form_hash` or `rack.request.query_hash`, and every later Request built from the same `env` reads that cache. The flip side, since Rack 3.2, is that rewriting `QUERY_STRING` afterwards does not refresh it.
- What does Rack::Response#finish return for a 204 status that was given a body?For statuses without an entity body - 1xx, 204, 304 - `finish` deletes `content-type` and `content-length`, closes the body, and returns `[204, headers, []]`, so the response stays valid under the Rack SPEC, which forbids those headers for these statuses.
saying these in an interview costs you the question
- Believing Rack::Request copies env, so edits through it are lost
- Using request["name"] to read a parameter on Rack 3.2
- Expecting a query parameter to win over a form field of the same name in params
- Returning the Rack::Response object itself instead of res.finish
- Setting headers as Content-Type on a Rack::Response and expecting a capitalised key