-
Notifications
You must be signed in to change notification settings - Fork 217
Middleware
Within Goliath, all request and response processing can be done in fully asynchronous fashion. Hence, you will have to use Rack middleware that is async-aware.
Goliath ships with a number of common middleware files, which you can load into your application through the familiar Rack builder interface: use MiddlewareClass, options
. Let’s take a look at a simple example:
class Echo < Goliath::API
use Goliath::Rack::Params # parse & merge query and body parameters
use Goliath::Rack::DefaultMimeType # cleanup accepted media types
use Goliath::Rack::Formatters::JSON # JSON output formatter
use Goliath::Rack::Render # auto-negotiate response format
use Goliath::Rack::Heartbeat # respond to /status with 200, OK (monitoring, etc)
def response(env)
[200, {}, {response: params['echo']}]
end
end
Above we initialize Params
middleware which will parse the body and query-string params, and so on. Finally, in our response we return a Ruby hash, which is then converted by our formatter middleware (JSON) into a well-formed JSON response.
From version 0.9.1, the Rack::Reloader
plugin is automatically loaded in the development environment, which will reload the code on each request to help us avoid having to start and stop the API after each edit.
A common use case for all web-services is to accept some number of query or POST body parameters and validate their formatting, presence, etc. Goliath ships with a set of Validation
middleware helpers which will help simplify this process:
class Echo < Goliath::API
use Goliath::Rack::ValidationError # deprecated: required by other 2 in older versions of Goliath to catch and render validation errors
use Goliath::Rack::Validation::RequestMethod, %w(GET) # allow GET requests only
use Goliath::Rack::Validation::RequiredParam, {:key => 'echo'} # must provide ?echo= query or body param
def response(env)
[200, {}, 'Hello World']
end
end
In the example above, we specify two validators: only GET requests are accepted, and we set key
to be a required parameter. If either of these validations are not met, then Goliath will return an error to the client with a simple explanation of what criteria must be met. For full list of other supported validations such as numeric ranges, etc, check the source.
Unlike other Rack powered app servers, Goliath creates a single instance of the middleware chain at startup, and reuses it for all incoming requests. Since everything is asynchronous, you can have multiple requests using the middleware chain at the same time. If your middleware tries to store any instance or class level variables they’ll end up getting stomped all over by the next request. Everything that you need to store needs to be stored in local variables.
Hence to make your custom middleware work with Goliath, you will have to (a) handle the asynchronous response case, and (b) make sure that the middleware is safe to reuse between multiple requests. The `Goliath::Rack::AsyncMiddleware` helper does this for you. As an example, let’s look at a simple JSON formatter middleware:
require 'yajl'
module Goliath
module Rack
module Formatters
class JSON
include Goliath::Rack::AsyncMiddleware
def post_process(env, status, headers, body)
if json_response?(headers)
body = Yajl::Encoder.encode(body, :pretty => true, :indent => "\t")
end
[status, headers, body]
end
def json_response?(headers)
headers['Content-Type'] =~ %r{^application/(json|javascript)}
end
end
end
end
end
Whoa! Where’d call()
go? It’s handled by AsyncMiddleware
. If you want to do any pre-processing, just override the call()
method, and invoke super
as the last line:
def call(env)
aq = get_awesomeness_quotient(Time.now)
super(env, aq)
end
def post_process(env, status, headers, body, aq)
new_body = make_totally_awesome(body, aq)
[status, headers, new_body]
end
The extra args (in this case, the awesomeness quotient) are passed to post_process
by the AsyncMiddleware. That’s one way to work around the ban on instance variables; the other is to use SimpleAroundware (see docs).
Let’s dive deeper. AsyncMiddleware
does the following:
module Goliath
module Rack
module AsyncMiddleware
include Goliath::Rack::Validator
def call(env, *args)
hook_into_callback_chain(env, *args)
downstream_resp = @app.call(env)
if final_response?(downstream_resp)
status, headers, body = downstream_resp
post_process(env, status, headers, body, *args)
else
return Goliath::Connection::AsyncResponse
end
end
# Put a callback block in the middle of the async_callback chain:
# * save the old callback chain;
# * have the downstream callback send results to our proc...
# * which fires old callback chain when it completes
def hook_into_callback_chain(env, *args)
async_callback = env['async.callback']
# The response from the downstream app is sent to post_process
# and then directly up the callback chain
downstream_callback = Proc.new do |status, headers, body|
new_resp = safely(env){ post_process(env, status, headers, body, *args) }
async_callback.call(new_resp)
end
env['async.callback'] = downstream_callback
end
end
end
end
The most important magic happens in hook_into_callback_chain, invoked by call(env)
. It
- stores the previous
async.callback
into local variableasync_callback
- prepares a new callback: the new callback invokes
post_process
(turning any of its errors into 404, 500, etc) and then sends the result to the upstreamasync_callback
we were given. - Finally, it redefines the terminal
async.callback
to be our own – this way, when the asynchronous response is done, Goliath can “unwind” the request by walking up the callback chain.
However, you will notice that AsyncMiddleware also executes the post_process
method in the default return case. If the next middleware is something like the heartbeat
middleware (returns 200 ‘OK’ if the path is ‘/status’), or if validations fail later in the middleware chain, the response will come back up through the chain directly; your endpoint’s response(env)
method will not be executed, and the callback chain will never be triggered.