Understanding Rack and Rack Middleware

All you need to know about Rack and Rack Middleware.

Shashwat Srivastava
4 min readMar 17, 2020

All requests to a web application framework written in RUBY go through a bunch of middleware before actually reaching a controller and similarly response is returned back via all middleware in the stack. Middleware performs various actions such as Logging requests, Response time measurement, Assigning unique id to request, Handling exceptions, Cookies management, etc.. so before getting into all these middleware it’s important to understand rack and rack middleware.

What is Rack?
The rack is a layer between the ruby frameworks(Rails, Sinatara, etc..) & the ruby application servers(Puma, unicorn, etc..).

The rack is an underlying technology behind nearly all of the web frameworks in the Ruby world. Rack defines a very simple interface, and any code that conforms to this interface can be used in a Rack application. The rack is distributed as a Ruby gem it gives the base over which our ruby application can be built easily.

Request-Response cycle

Rack Interface

The rack has a very simple interface, Rack-based application must include the following three characteristics:
1- It must respond to the call method.
2- The call method must accept a single argument — This argument is typically called env or environment, and it bundles all of the data about the request.
3- The call method must return an array of three elements these elements are, in order, status for the HTTP status code, headers, and body for the actual content of the response.

#config.ruclass MyRackApp
def call(env)
[200, {“Content-Type” => “text/plain”}, [“Hello World”]]
end
end
run MyRackApp.new

on running rackup command it looks for config.ru file in the root directory and starts a server on default TCP port 9292.

Rack Middleware

Middleware is the building blocks of larger applications built using the Rack standards. Each middleware is a Rack compatible application, and our final application is built by nesting all these middleware.

class FilterLocalHost
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
if req.ip == "127.0.0.1" || req.ip == "::1"
[403, {}, ["forbidden"]]
else
@app.call(env)
end
end
end
class FilterRoute
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
if req.path == "/"
@app.call(env)
else
[404, {}, ["not found"]]
end
end
end
app = Rack::Builder.new do |builder|
builder.use FilterLocalHost
builder.use FilterRoute
builder.run MyRackApp.new
end
run app

In this example we have three Rack applications:

  • One for the IP check (FilterLocalHost)
  • One for filtering routes (FilterRoutes)
  • One for the application itself to deliver the content (HTML, JSON, etc)

Rack::Builder is used to chain the application & middleware so they can work together.
use adds middleware to the stack, run dispatches to an application.

@app.call doesn’t terminate the execution of your handler — it just calls the next middleware in the chain. It is expected that each middleware will either call the next in the chain and return its return value or terminate the chain by returning an array of [status_code, body, headers]. Each middleware is expected to pass the array of [status_code, body, headers] back up the chain, by returning that value out of its #call method.

env is just a hash. Rack and middleware can add values to it. Some common env key-values.

{
“GATEWAY_INTERFACE” => “CGI/1.2”,
“PATH_INFO” => “/”,
“QUERY_STRING” => “”,
“REMOTE_ADDR” => “::1”,
“REMOTE_HOST” => “localhost”,
“REQUEST_METHOD” => “GET”,
“REQUEST_URI” => “http://localhost:9292/",
“SCRIPT_NAME” => “”,
“SERVER_NAME” => “localhost”,
“SERVER_PORT” => “9292”,
“SERVER_PROTOCOL” => “HTTP/1.1”,
“HTTP_HOST” => “localhost:9292”,
“HTTP_USER_AGENT” => “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36”,
“HTTP_ACCEPT” => “text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8”,
“HTTP_ACCEPT_LANGUAGE” => “zh-tw,zh;q=0.8,en-us;q=0.5,en;q=0.3”,
“HTTP_ACCEPT_ENCODING” => “gzip, deflate”,
“HTTP_COOKIE” => “jsonrpc.session=3iqp3ydRwFyqjcfO0GT2bzUh.kkskskkskskskksksksksksksssls”,
“HTTP_CONNECTION” => “keep-alive”,
“HTTP_CACHE_CONTROL” => “max-age=0”,
“rack.version” => [1, 2],
“rack.input” => #<StringIO:0x007fa1bce039f8>,
“rack.errors” => #<IO:<STDERR>>,
“HTTP_VERSION” => “HTTP/1.1”,
“REQUEST_PATH” => “/index.html”
}

Rack::Request provides a convenient interface to a Rack environment. It is stateless, the environment env passed to the constructor will be directly modified.
request = Rack::Request.new(env)

Methods available on request object:

[:params, :update_param, :delete_param, :[], :[]=, :path, :values_at, :host, :fullpath, :scheme, :port, :authority, :url, :body, :delete?, :GET, :POST, :ip, :accept_encoding, :cookies, :path_info, :content_type, :script_name, :script_name=, :path_info=, :request_method, :query_string, :content_length, :logger, :user_agent, :multithread?, :referer, :referrer, :session, :session_options, :get?, :head?, :options?, :link?, :patch?, :post?, :put?, :trace?, :unlink?, :xhr?, :host_with_port, :ssl?, :media_type, :media_type_params, :content_charset, :form_data?, :parseable_data?, :base_url, :accept_language, :trusted_proxy?, :env, :has_header?, :get_header, :fetch_header, :each_header, :set_header, :add_header, :delete_header, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, :singleton_method, :instance_variable_defined?, :define_singleton_method, :method, :public_method, :instance_variable_set, :extend, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :object_id, :send, :display, :to_s, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variable_get, :instance_variables, :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]

What can we do in rack middleware?

We can use middleware to keep non-app specific logic separate like:

  • Caching headers
  • Logging
  • Parsing request object
  • Performance & Usage Monitoring
  • Modifying response etc..

************************* Thank You ****************************

reference: https://www.rubyguides.com/2018/09/rack-middleware/

Stay tuned, We will be going through the complete journey of rack middleware and see how our request to rails application server becomes a response.

--

--