ruby / web framework
I used a 500-line web framework built on Rack. The application served HTML with HTML templates, queried Postgres via DB, ran Rack middleware, and queued background work via job queues.
Why Rack
Rails adds many dependencies and abstractions. A small Rack framework contains only what the application needs. Stack traces point at application code. The small surface also fits in an AI context window.
Rack app with routing
A Rack app maps URL patterns to handler classes. The router stores each handler class and its constructor arguments at boot. It creates a new instance per request, so threads do not share state:
module Framework
class App
def initialize
@routes = {"GET" => [], "POST" => []}
end
def get(path, klass, *args, **kwargs)
@routes["GET"] << [compile(path), path, klass, args, kwargs]
end
def post(path, klass, *args, **kwargs)
@routes["POST"] << [compile(path), path, klass, args, kwargs]
end
def redirect(from, to)
@routes["GET"] << [compile(from), from, :redirect, [to], {}]
end
# Rack interface
def call(env)
req = Rack::Request.new(env)
result = match(req.request_method, req.path_info)
if result.nil?
return [404, {"Content-Type" => "text/html"}, [File.read("public/404.html")]]
end
klass, args, kwargs, params = result
if klass == :redirect
return [301, {"Location" => args[0]}, []]
end
env["router.params"] = params
response = catch(:halt) do
klass.new(*args, **kwargs).call(env)
end
response
end
private def match(method, path)
@routes[method]&.each do |pattern, _, klass, args, kwargs|
if (m = pattern.match(path))
return [klass, args, kwargs, m.named_captures]
end
end
nil
end
private def compile(path)
pattern = path.gsub(/:([a-z_]+)/, '(?<\1>[^/]+)')
Regexp.new("\\A#{pattern}\\z")
end
end
end
I register routes at boot:
app = Framework::App.new
app.get "/health", HealthHandler::Check, db
app.get "/companies/:id", CompaniesHandler::Show, db
app.post "/companies/create", CompaniesHandler::Create, db
app.redirect "/old-path", "/new-path"
run app
Path parameters such as :id become named captures
in env["router.params"].
catch(:halt) lets a handler stop early with throw :halt, response.
require_login uses it to redirect.
Handlers
Subclasses implement handle.
The base class does setup and authentication:
module Framework
class Handler
attr_reader :db, :req, :params, :session
def initialize(db)
@db = db
end
def call(env)
setup(env)
require_login
resp = handle
resp = wrap_html(resp)
resp
end
def setup(env)
@env = env
@req = Rack::Request.new(env)
@params = @req.params.merge(env["router.params"] || {})
@session = env["rack.session"] || {}
end
def handle
raise NotImplementedError, "#{self.class} must implement #handle"
end
# Returns an HTML string. For partials and composition.
def render(template, **locals)
all_locals = locals.merge(default_locals)
Template.render(template, all_locals, layout: nil)
end
# Returns a Rack response with layout. For full page renders.
def page(template, title: "App", **locals)
all_locals = locals.merge(default_locals).merge(title: title)
html = Template.render(template, all_locals, layout: "layouts/application")
[200, {"Content-Type" => "text/html"}, [html]]
end
def redirect(location)
if !same_origin_or_internal_path?(location)
return head(400)
end
[303, {"Location" => location}, []]
end
private def same_origin_or_internal_path?(location)
if internal_only_path?(location)
return true
end
target = URI.parse(location)
base = URI.parse(base_url)
target.scheme == base.scheme && target.host == base.host && target.port == base.port
rescue URI::InvalidURIError
false
end
private def internal_only_path?(path)
if path.nil?
return false
end
path.start_with?("/") && !["/", "\\", "@"].include?(path[1])
end
def head(status, headers = {})
[status, headers, []]
end
private def require_login
if current_user
return true
end
@session[:return_to] = @req.fullpath
throw :halt, redirect("/login")
end
# Auto-wrap string returns from handle into Rack responses.
private def wrap_html(resp)
if resp.is_a?(String)
[200, {"Content-Type" => "text/html"}, [resp]]
else
resp
end
end
private def default_locals
{
current_user: current_user,
params: params,
session: @session,
flash: @env["app.flash"] || {}
}
end
end
end
redirect checks the location to block open redirects.
Three base classes handle authentication:
Handler: requires loginPublicHandler: skips loginAdminHandler: requires admin credentials
A health check handler:
module HealthHandler
class Check < Framework::PublicHandler
def handle
db.exec("SELECT 1")
head 200
end
end
end
A page handler:
module CompaniesHandler
class Show < Framework::Handler
PageData = Data.define(:name, :status)
def handle
co = Companies::Find.new(db).call(id: params["id"])
if co.nil?
return [404, {"Content-Type" => "text/html"}, [File.read("public/404.html")]]
end
data = PageData.new(
name: co["name"],
status: co["status"]
)
page "companies/show", title: data.name, data: data
end
end
end
Encrypted cookies
I encrypt the authentication token cookie with AES-256-GCM:
class EncryptedCookieJar
def initialize(request_cookies, secret)
@request_cookies = request_cookies
@key = OpenSSL::Digest::SHA256.digest(secret)[0, 32]
end
def [](name)
data = @request_cookies[name]
if data.nil?
return nil
end
decrypt(data)
end
private def encrypt(plaintext)
cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt
cipher.key = @key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
Base64.urlsafe_encode64(iv + tag + ciphertext)
end
private def decrypt(data)
raw = Base64.urlsafe_decode64(data)
cipher = OpenSSL::Cipher.new("aes-256-gcm").decrypt
cipher.key = @key
cipher.iv = raw[0, 12]
cipher.auth_tag = raw[12, 16]
cipher.update(raw[28..]) + cipher.final
rescue
nil
end
end
Rack::Session::Cookie signs the session cookie.
Only the authentication token uses the encrypted jar.
Middleware
Each middleware is a class with initialize(app) and call(env).
CSRF
class CSRF
SAFE_METHODS = %w(GET HEAD OPTIONS TRACE).freeze
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
if !SAFE_METHODS.include?(req.request_method)
session_token = req.session[:csrf_token]
param_token = req.params["authenticity_token"]
header_token = env["HTTP_X_CSRF_TOKEN"]
if session_token.nil? || (param_token != session_token && header_token != session_token)
req.session.clear
return [303, {"Location" => "/login"}, []]
end
end
req.session[:csrf_token] ||= SecureRandom.base64(32)
@app.call(env)
end
end
Flash messages
class Flash
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
env["app.flash"] = req.session[:flash] || {}
req.session.delete(:flash)
@app.call(env)
end
end
The flash displays once because the middleware deletes it from the session.
Content Security Policy
See Content Security Policy for the header and the nonce middleware.
HTTPS redirect with HSTS
class HTTPS
def initialize(app)
@app = app
end
def call(env)
scheme = env["HTTP_X_FORWARDED_PROTO"] || Rack::Request.new(env).scheme
if scheme == "http"
url = "https://#{env["HTTP_HOST"]}#{env["REQUEST_URI"]}"
return [301, {"Location" => url}, []]
end
status, headers, body = @app.call(env)
headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
[status, headers, body]
end
end
Error handling
class ErrorHandler
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
rescue => e
Sentry.capture_exception(e)
if ENV["APP_ENV"] == "development"
[500, {"Content-Type" => "text/html"}, [<<~HTML]]
<h1>500</h1>
<pre>#{CGI.escapeHTML(e.message)}\n#{CGI.escapeHTML(e.backtrace.join("\n"))}</pre>
HTML
else
[500, {"Content-Type" => "text/html"}, [File.read("public/500.html")]]
end
end
end
Request logging
class RequestLogger
def initialize(app)
@app = app
end
def call(env)
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, body = @app.call(env)
path = env["PATH_INFO"]
if path != "/health"
ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
puts "#{status} #{env["REQUEST_METHOD"]} #{path} #{ms}ms"
end
[status, headers, body]
end
end
Composing the stack
Rack::Builder composes the middleware stack with the app:
module Framework
module Server
def self.app(db)
routes = build_routes(db) # returns Framework::App
Rack::Builder.new do
use RequestLogger
use ErrorHandler
use HTTPS if ENV["APP_ENV"] == "production"
use Rack::Session::Cookie,
key: "_session",
secret: ENV.fetch("SECRET_KEY_BASE"),
same_site: :lax,
secure: ENV["APP_ENV"] == "production"
use SafeStaticCache,
urls: ["/css", "/js", "/fonts", "/images"],
root: "public",
header_rules: [[:all, {"Cache-Control" => "public, max-age=31536000, immutable"}]]
use CSRF
use Flash
run routes
end.to_app
end
end
end
routes returns the app without middleware, for handler tests:
def self.routes(db)
app = Framework::App.new
app.get "/health", HealthHandler::Check.new(db)
# ... more routes
app
end
Content Security Policy
The Content-Security-Policy header tells the browser which origins
can load scripts, styles, and frames. A strict policy limits the damage
from XSS. I start from default-deny and list what the application
loads:
[
"default-src 'self'",
"base-uri 'none'",
"object-src 'none'",
"frame-ancestors 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https://your-image-cdn.example",
"connect-src 'self'"
].join("; ")
A new external host, such as an analytics script, requires an edit to this list.
Pitfalls
'unsafe-eval' enables eval, new Function, and setTimeout with a
string body. The application does not need them.
https: in script-src accepts any HTTPS origin, which removes most
of the value of CSP. I list specific hosts.
'unsafe-inline' in script-src allows inline <script> tags and
event handlers, the most common XSS vector. I serve scripts from files
or use a nonce.
Nonces for inline
When an inline script must pass server data to JavaScript, the CSP
middleware generates a nonce per response:
class CSP
def initialize(app)
@app = app
end
def call(env)
nonce = SecureRandom.base64(16)
env["app.csp_nonce"] = nonce
status, headers, body = @app.call(env)
if headers["Content-Type"].to_s.include?("text/html")
headers["Content-Security-Policy"] = [
"base-uri 'none'",
"object-src 'none'",
"script-src 'self' 'nonce-#{nonce}'",
"style-src 'self' 'unsafe-inline'"
].join("; ")
end
[status, headers, body]
end
end
The handler passes the nonce to templates as a default local:
%script{nonce: csp_nonce, type: "application/json", id: "boot"}
!= bootstrap_data_json
The browser executes the inline tag only if its nonce matches the response header.
Report-only first
Before I tighten a policy, I ship it as
Content-Security-Policy-Report-Only for one release. The browser
reports violations and blocks nothing. Then I fix the sources it
reports and enforce.
CSP tests
A request test checks the header on every HTML response:
def test_csp_excludes_unsafe_eval
resp = get("/")
csp = resp.headers["Content-Security-Policy"].to_s
ok { csp.include?("script-src 'self'") }
ok { !csp.include?("unsafe-eval") }
ok { !csp.match?(/script-src[^;]*\bhttps:\B/) }
end
A change that adds unsafe-eval fails this test.
Boot sequence
I require each gem explicitly.
Bundler.require hides the dependency graph and slows boot.
Running with Puma
require "puma"
require_relative "lib/db"
require_relative "lib/framework/boot"
require_relative "lib/framework/server"
DB.configure do |c|
c.pool_size = ENV.fetch("WEB_CONCURRENCY").to_i * ENV.fetch("WEB_THREADS").to_i
c.reap = true
c.statement_timeout_ms = 24_000
end
conf = Puma::Configuration.new do |c|
c.app Framework::Server.app(DB.pool)
c.environment ENV.fetch("APP_ENV")
c.bind "tcp://0.0.0.0:#{ENV.fetch("PORT")}"
c.threads ENV.fetch("WEB_THREADS").to_i, ENV.fetch("WEB_THREADS").to_i
c.workers ENV.fetch("WEB_CONCURRENCY").to_i
c.cluster do
c.preload_app!
c.before_worker_boot do
DB.reset_pool! # fresh connections after fork
end
c.shutdown_debug on_force: true
end
end
Puma::Launcher.new(conf).run
Timeout chain
A hung request holds a Puma thread and a database connection. Each layer sets a lower timeout than the layer outside it:
use Rack::Timeout, service_timeout: 25, term_on_timeout: 1
Rack::Timeout25s: the outer limit.term_on_timeout: 1sends the worker SIGTERM instead ofThread#raise, which can leave locks in a bad state. Puma cluster mode restarts the worker.shutdown_debug on_force: truelogs thread backtraces when Puma must force-kill the worker.- Postgres
statement_timeout24s: cancels a slow query beforeRack::Timeoutfires. The application rescuesPG::QueryCanceled. - HTTP client timeouts under 25s on every outbound call.
term_on_timeout requires Puma cluster mode (WEB_CONCURRENCY >= 2).
Testing handlers
Handler tests use Rack::Test against routes.
See ruby / test framework.
class HandlerTest < Test
include Rack::Test::Methods
def app
Framework::Server.routes(db)
end
def sign_in
set_cookie("remember_token=test")
end
end
class HealthTest < HandlerTest
def test_health
resp = get("/health")
ok { resp.status == 200 }
end
end
class CompaniesShowTest < HandlerTest
def test_show
sign_in
co = insert_company(name: "Acme Inc")
resp = get("/companies/#{co.id}")
ok { resp.status == 200 }
ok { resp.body.include?("Acme Inc") }
end
end
Response modes
A handler declares its response modes:
# Ajax-only (forms, partials)
class CompaniesHandler::Edit < Framework::Handler
modes :ajax
def handle
render "companies/edit", data: data
end
end
# Dual-mode (full page + ajax partial)
class CompaniesHandler::Show < Framework::Handler
modes :html, :ajax
def handle
if ajax?
render "companies/ajax_show", data: data
else
page "companies/show", title: data.name, data: data
end
end
end
# HTML-only (default; no modes declaration needed)
class DashHandler::Home < Framework::Handler
def handle
page "dash/home", title: "Home", data: data
end
end
The base class returns 400 for a request in an undeclared mode.
Ping test coverage in ruby / test framework checks that every mode has a test.
HTTP methods
The router accepts GET and POST only. HTML forms submit only those two.