Skip to content
mrflip edited this page Jul 31, 2011 · 12 revisions

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.

Using Rack Middleware in Goliath

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.

Validations

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.

Writing your own middleware

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(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(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 variable async_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 upstream async_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.