In a Sinatra payment-webhook receiver, how would you use a before filter and a helper to reject requests whose signature fails to verify?
answer
- same app instance as the route
- before '/webhooks/*' scopes the filter
- a filter's return value is ignored
- halt throws :halt out of helpers
- after filters still run
basics
~10 sDefine a helper that computes an HMAC of the raw body and calls halt 401 on a mismatch, then call it from before '/webhooks/*'. The halt skips the route, while after filters still run.
solid answer
~40 s`helpers do ... end` evaluates its methods into the app class, so routes, filters and templates can all call `verify_signature!`. A `before '/webhooks/*'` filter runs ahead of the route on the same per-request app instance: it reads the raw body once with `request.body.read`, keeps it in `@payload` and calls the helper. The helper compares `OpenSSL::HMAC.hexdigest` of the payload with the signature header using `Rack::Utils.secure_compare` and calls `halt 401` on a mismatch. `halt` throws `:halt`, which unwinds through the helper and the filter to Sinatra's `catch`, so the remaining filters and the route never run. A filter's return value is ignored, so returning `false` stops nothing. After filters run even after a halt, which makes them a good place to log the final status.
code
ruby · 28 linesrequire 'sinatra/base'
require 'json'
require 'openssl'
class WebhookApp < Sinatra::Base
helpers do
def verify_signature!(payload)
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), payload)
given = env['HTTP_X_SIGNATURE'].to_s
halt 401, 'bad signature' unless Rack::Utils.secure_compare(expected, given)
end
end
before '/webhooks/*' do
@payload = request.body.read
verify_signature!(@payload)
end
post '/webhooks/payments' do
event = JSON.parse(@payload)
halt 422 unless event['id']
204
end
after '/webhooks/*' do
warn "#{request.path_info} -> #{response.status}"
end
endgo deeper
Recall that before runs ahead of the route, after runs behind it, helpers define shared methods, and halt ends the request with a status and body.
Explain that filters, routes and templates share one per-request instance, that halt is a throw unwinding through helpers, and that after filters still run.
Show the production shape: verify the raw body once, compare in constant time, scope the filter to webhook paths, and log the final status in an after filter.
Weigh a Sinatra filter against a separate Rack middleware for verification: the filter sits beside its routes, the middleware can guard several apps at once.
## Filters, helpers and the request instance A **filter** is a block Sinatra runs around the routes. `before` blocks run ahead of the matching route and `after` blocks behind it. Both accept an optional path pattern, `before '/webhooks/*'`, and the same conditions routes take; without a pattern a filter runs for every request. A **helper** is an ordinary instance method. `helpers do ... end` evaluates the block inside the application class, and `helpers SignatureHelpers` includes a module, so the methods are callable from routes, filters and templates alike. What ties these together is **the instance they run on**. The class-level `call` hands each request to a prototype instance, whose `call` does `dup.call!(env)`. Every request therefore gets its own copy of the application object, and its before filters, route, templates and after filters all run with that copy as `self`. That is why an instance variable set in a before filter, such as `@payload`, is visible in the route, and why two concurrent requests on a threaded server cannot see each other's. ## The order of execution For each request Sinatra: 1. serves a matching file from `public_folder` if static serving is on; 2. runs the `before` filters, superclass filters first, then in definition order, each only when its pattern matches; 3. runs the first matching route; 4. runs the `after` filters from an `ensure` clause, so they run whether the route returned, halted or raised (only a request answered by a static file skips them). ## The webhook receiver ```ruby before '/webhooks/*' do @payload = request.body.read # raw bytes, read once verify_signature!(@payload) # may halt 401 end post '/webhooks/payments' do event = JSON.parse(@payload) halt 422 unless event['id'] 204 end ``` The helper computes `OpenSSL::HMAC.hexdigest('SHA256', secret, payload)`, takes the provider's signature from `env`, and compares them with `Rack::Utils.secure_compare`, a comparison whose time does not depend on where the strings differ. On a mismatch it calls `halt 401, 'bad signature'`. Why sign the **raw** body: the provider signed bytes, not a parsed Hash. Parsing and re-serialising can reorder keys or change whitespace, and the signature no longer matches. Reading it in the filter and storing it in `@payload` also avoids a second `request.body.read`, which at end of input returns an empty string. This works for a JSON body because Sinatra does not parse JSON into `params`. ## Why a filter and not a call in each route A receiver often grows one route per provider or per event family: `post '/webhooks/payments'`, `post '/webhooks/refunds'`. Calling `verify_signature!` at the top of each route works, but the first route someone adds without it is unauthenticated. A `before '/webhooks/*'` filter puts the check on the path prefix, so every current and future webhook route is covered and the routes contain only business logic. The filter still lives inside the Sinatra app, next to the routes it protects. The alternative, a separate Rack middleware placed in front of the app, suits a check that several applications share; the filter suits one that belongs to this app alone. ## What stops a request and what does not | Inside a before filter | Effect | |---|---| | return `false` or `nil` | ignored; the next filter and the route still run | | `halt 401, 'bad signature'` | remaining before filters and the route are skipped; after filters run | | `pass` | ends only this filter; the next filter and the route still run | | `raise` an exception | Sinatra's error handling takes over, a 500 unless a handler says otherwise | `halt` works from inside a helper because it is `throw :halt`: Ruby unwinds every method frame between the `throw` and Sinatra's `catch(:halt)`, so there is no need to check a return value in the filter. ## Pitfalls in review - **An unscoped filter.** `before do` also runs for a health check or a static asset; scope verification with a pattern. - **Comparing with `==`.** Its running time can depend on how many leading bytes match, which leaks information; use `Rack::Utils.secure_compare`. - **Relying on a return value.** Only `halt` (or `error`, `not_found` and `redirect`, which call it) stops the request. - **Parsing before verifying.** Verify the signature on the exact bytes first, then call `JSON.parse`. - **Logging in the route.** A rejected request never reaches it; an `after '/webhooks/*'` filter sees every final status, 401 included.
- Why can the before filter hand @payload to the route in an instance variable, even on a threaded server?Filters, the route and templates run on the same application instance for a request, so they share its instance variables. That instance is per request: the class-level `call` delegates to a prototype whose `call` does `dup.call!(env)`, so concurrent requests each work on their own copy and never see each other's `@payload`.
- What does calling pass inside a before filter do?It ends only that filter. Sinatra runs each filter inside a `catch(:pass)`, so `pass` skips the rest of that block while the next filter and the route still run. `pass` means "try the next matching route" only inside a route block.
- How would you share the signature helper across several modular apps?Put the method in a plain module and call `helpers SignatureHelpers` in each app class; with module arguments, `helpers` simply includes them. `register` is different: it extends the class with an extension module to add class-level DSL methods and calls the module's `registered` hook.
A before filter is the doorman at a club: he checks every ticket before anyone reaches the bar (the route) and can turn a guest away on the spot (halt). The cloakroom attendant at the exit (the after filter) still sees everyone who leaves, admitted or turned away.
saying these in an interview costs you the question
- Returning false from a before filter rejects the request.
- Calling halt inside a helper only leaves the helper, so the route still runs.
- After filters are skipped once a before filter has called halt.
- Instance variables set in a before filter are not visible in the route.
- Methods in a helpers block become class methods reached through settings.