skip to content

In a Sinatra route block, what is the difference between halt, pass and redirect, and what response does each one produce?

level: middleimportance: should knowfreq 50%

answer

  1. throw and catch, not exceptions
  2. halt: status, headers, body in any mix
  3. pass: next matching route, else 404
  4. redirect: 302, or 303 for non-GET
  5. redirect's extra arguments go to halt

basics

~20 s

halt stops the request at once and sends the status, headers and body you give it. pass abandons the current route so the next matching route answers, or a 404. redirect sets Location and a 302 or 303, then halts.

solid answer

~40 s

All three are control flow built on Ruby's `throw`/`catch`, so code after them never runs, even when they are called from a helper. `halt` throws `:halt` with its arguments, such as `halt 410`, `halt 'text'`, `halt 401, 'go away'` or `halt 402, {'content-type' => 'text/plain'}, 'pay'`, and Sinatra builds the response from them. `pass` throws `:pass`: Sinatra leaves the current route and keeps looking for another route matching the same request; if none does, the result is a 404, or, when the app runs as middleware, the request goes downstream. `redirect(uri, *args)` sets 303 for a non-GET request on HTTP/1.1 and 302 otherwise, puts an absolute URL in `Location`, then calls `halt(*args)`, so `redirect to('/done'), 301` overrides the status.

code

ruby · 15 lines
ruby
require 'sinatra'

post '/webhooks/:provider' do
  pass unless params['provider'] == 'payments'
  halt 400, 'empty payload' if request.body.read.empty?
  204
end

post '/webhooks/*' do
  halt 404, 'unknown provider'
end

get '/webhooks' do
  redirect to('/webhooks/status'), 301   # extra argument goes to halt
end

go deeper

for a junior

Recall the three outcomes: halt answers now, pass tries the next matching route, redirect sends the client to another URL.

for a middle

Explain the throw/catch mechanism, the argument forms halt accepts, what pass does with no other route, and redirect's 302 versus 303 rule.

for a senior

Show the traps from production: code after redirect, 404 via pass, redirects ignoring the mount path, and halting from helpers inside filters.

for a principal

Frame when control-flow helpers keep a service readable and when explicit response objects or separate apps would make the flow easier to follow.

## One mechanism under three helpers Sinatra wraps request processing in `catch(:halt)`, inside a private method named `invoke`, and runs each matching route inside `catch(:pass)`. Ruby's `throw` unwinds every frame between it and the matching `catch` without being an exception: no `rescue` fires and no error handler runs. That gives the three helpers their shared properties: - code after the call **never runs**; - they work from a filter or from a **helper method** several calls deep, because the throw unwinds through those frames; - after filters still run, since Sinatra calls them from an `ensure` clause. ## halt: answer now `halt(*response)` throws `:halt` with its arguments, and `invoke` turns them into a response: 1. an Integer sets the status: `halt 410`; 2. a String sets the body: `halt 'maintenance'`; 3. an Array starting with an Integer sets the status from the first element, the body from the last and the headers from any Hash in between: `halt 401, 'go away'` or `halt 402, {'content-type' => 'text/plain'}, 'pay first'`; 4. anything else that responds to `each` becomes the body, so `halt erb(:error)` works. A bare `halt` keeps the current status (200 unless something set it) and sends an empty body. The helpers `error 403, 'nope'` and `not_found` set a body and call `halt` for you. ## pass: not my request after all `pass` throws `:pass`. Sinatra discards the current route and continues down the same verb's route list, then through routes a superclass defines, trying each pattern against the path. What happens next: - the next route whose pattern and conditions match runs as if it had matched first; - `pass { 'fallback' }` stores the block and runs it only if no other route matches; - if nothing else matches, Sinatra raises `Sinatra::NotFound`: a 404, with an `X-Cascade: pass` header because the `x_cascade` setting defaults to `true`; - when the app sits in front of another Rack app as middleware, the unmatched request is **forwarded** downstream instead of answered with 404. Inside a before filter `pass` only ends that filter. ## redirect: go elsewhere `redirect(uri, *args)` does three things: - sets the status to **303** when the request is not a GET and the protocol is HTTP/1.1, otherwise **302**; - sets `Location` through the `uri` helper (aliased `to` and `url`), which makes the address absolute because `absolute_redirects` defaults to `true`; - calls `halt(*args)`, so `redirect to('/done'), 301` or `redirect '/login', 'please sign in'` pass their extra arguments to `halt`. `redirect back` sends the client to the `Referer`. Mind the mount point: with `prefixed_redirects` false by default, `redirect '/status'` ignores the path the app is mounted under, while `redirect to('/status')` prepends it. ## In filters The helpers behave the same in filters, with one difference in reach: - `halt` in a **before** filter skips the remaining filters and the route; - `halt` or `redirect` in an **after** filter replaces the response the route produced, because the throw still reaches Sinatra's outer `catch(:halt)`; - `pass` in any filter only ends that filter's block. ## Side by side | | `halt` | `pass` | `redirect` | |---|---|---|---| | Throws | `:halt` | `:pass` | `:halt`, via `halt` | | Status | what you give it, else unchanged | whatever the next route produces, else 404 | 302, or 303 for non-GET on HTTP/1.1, unless overridden | | Later routes | never tried | next match tried | never tried | | After filters | run | run | run | ## Worked example ```ruby post '/webhooks/:provider' do pass unless params['provider'] == 'payments' halt 400, 'empty payload' if request.body.read.empty? 204 end post '/webhooks/*' do halt 404, 'unknown provider' end ``` A POST to `/webhooks/legacy` passes out of the first route and gets the second route's 404; an empty POST to `/webhooks/payments` gets 400; a good one gets 204. ## Mistakes to avoid - Writing code after `redirect` as if it will still run. - Expecting `redirect` to answer 301 by default. - Using `pass` as an early return; it hands the request to another route or a 404. - Rescuing to catch a halt: it is a throw, and `rescue` never sees it.

  • Which status does redirect use after a form POST, and how do you force a different one?
    On HTTP/1.1 a non-GET request gets 303 See Other, so the browser follows up with a GET; otherwise Sinatra uses 302. Extra arguments go to `halt`, so `redirect to('/done'), 307` sends 307 instead.
  • The app is mounted under /hooks. Where do redirect '/status' and redirect to('/status') send the client?
    `prefixed_redirects` defaults to false, so `redirect '/status'` points at `/status` on the host, outside the mount. `to('/status')` prepends `request.script_name`, giving `/hooks/status`. Use `to` or `url` for addresses inside the app.
  • What happens when pass leaves no other matching route?
    Sinatra raises `Sinatra::NotFound`, answered as a 404 with `X-Cascade: pass`, or runs a block given to `pass` if there was one. When the app is middleware in front of another Rack app, the request is forwarded downstream instead.

saying these in an interview costs you the question

  • Code after halt keeps running; halt only sets the status.
  • pass jumps to the next route even if its pattern does not match.
  • redirect only sets Location and lets the route block continue.
  • Sinatra's redirect answers 301 Moved Permanently by default.
  • When pass finds no other route, Sinatra answers 500.
  • A rescue clause in the route can catch a halt.