.. post:: 2014-04-11
:tags: OCaml, Opium
:author: Rudi Grinberg
Middleware in Opium
===================
In my `previous
post `__ I've
introduced opium in a beginner friendly way, while in this post I'll try
to show something that's a little more interesting to experienced OCaml
programmers or those are well versed with protocols such as Rack, WSGI,
Ring, from Ruby, Python, Clojure respectively.
Traditional Middleware
----------------------
First, let's start with some history. I'll be recounting from memory so
I apologize for any inaccuracies. Back in the stone ages of web
application development, a big problem was (still is) creating reusable
stand alone components. For example, caching, authentication, static
pages. One solution to this problem was invented by the python
community, (WSGI) and popularized by the Ruby community (Rack). Since
then, it caught on like wild fire and has been ported to almost every
other langauge. I attribute Rack's success to its extreme simplicity. In
Rack, an application is an object that takes an environment, an
all-encompassing dictionary that includes the http request among other
things, usually called ``env`` and returns a tuple of three elements,
the status code, the headers, and the body of the response. Translating
this to OCaml, a rack application is just:
.. code-block:: ocaml
type env = (string, string) Hashtbl.t
(* In ruby, the env hash is not restricted to string values course. They can be
any values. Ignore this restriction for now. Or pretend that we're doing some
gross Obj.magic hack. Ruby does the same anyway ;) *)
type body = string
type header = (string, string) Hashtbl.t
type application = env -> status * header * body
(* Actually I'm simplfiying. In reality, body in Rack is more similar to: *)
type body = (string -> unit) -> unit
Accepting Rack's proposition that applications are simply functions that
return that 3 element tuple, how do we create reusable components? In
Rack the solution was to create so called "middleware". Middleware is an
object with a call method (of type application) and is constructed by
passing the next application down the middleware chain. A very literal
translation of a typical rack middleware to OCaml gives:
.. code-block:: ocaml
class my_middleware app =
object
method call env =
if go_down_chain_condition
then app#call env
else (failwith "fix", failwith "me", failwith "please")
end
One can imagine chaining multiple middleware to provide various
functionality. For example checking credentials in the header of the
request (found in env) to decide whether to authenticate the user to
proceed with the next step or to return a not authorized status.
Middleware in Opium
-------------------
Of course, nobody sane writes code like that in OCaml. So if we get rid
of the classes and store state using closures instead of instance
variables (if necessary) we'd get just a function of type:
.. code-block:: ocaml
type middleware = application -> env -> status * header * body
(* which can be simplified to *)
type middleware = application -> application
In this viewpoint, middleware is simply a higher order function that
transforms an application to another.
OK, so is this good enough for OCaml? No. Putting back my statically
typed functional programming hat on I can poke a couple of holes in this
approach.
- env is mutable. In fact, middleware is encouraged to treat it as
request local storage and pass information between middleware. This
means that env may not be the same before and after calling a
middleware.
- env offers no encapsulation. Middleware can easily pry at each
other's internals. Sometimes this is necessary, but many times it's
not. Ring in clojure offers namespaced keywords as the keys to the
env hash, but this is only a gentleman's agreement.
- env offers no type safety. In rack, doing env['xxx'] is like trying
to pull a rabbit out of hat. There's no guarantee that the value
obtained will of a certain type.
Universal Maps
~~~~~~~~~~~~~~
All of this points us towards using something other than an untyped hash
table for the environment hash. But what do we use in OCaml if we want,
an openly extensible, heteregoneus, immutable map? We use core's
``Univ_map``. I won't go into the details of how it works but I'll say
that a univ map supports the following two operations:
.. code-block:: ocaml
val find : Univ_map.t -> 'a Univ_map.Key.t -> 'a option
val add : Univ_map.t -> 'a Univ_map.Key.t -> 'a -> Univ_map.t
In addition to the creation of new ``Univ_map.Key.t`` that are
associated with the types a key would extract.
.. code-block:: ocaml
val create: name:string -> ('a -> Sexp.t) -> 'a Univ_map.Key.t
If you'd like to know more I recommend the following resource [2], [3],
[4].
In Opium, we throw away Rack's env and simply put everything under the
same umbrella and call it a Request. Similarly, an opium response will
subsume the 3 element response tuple. ``env`` will then be the
extensible part of a request/response. Which gives us:
.. code-block:: ocaml
type request = {
request: Cohttp.Request.t;
env: Univ_map.t;
}
type response = {
code: Code.status_code;
headers: Cohttp.Header.t;
body: Cohttp_async.Body.t;
env: Univ_map.t;
}
Now middleware is able to:
- store stuff in env and always know the type of a value pulled out of
env.
- middleware can encapsulate its private data by not exposing its
``env`` keys in the public interface
And of course, there's no sight of side effects anywhere.
Examples
--------
Enough theory crafting, let's build some middleware. Let's start with a
trivial example. First of all, our middleware need not even use ``env``
at all. For example here's a middleware that uppercases the body:
.. code-block:: ocaml
open Core.Std
open Async.Std
open Opium.Std
let uppercase =
let filter handler req =
handler req >>| fun response ->
response
|> Response.body
(* this trick is only available with the latest cohttp.
you can manually unpack to a string and then repack however *)
|> Cohttp_async.Body.map ~f:String.uppercase
|> Field.fset Response.Fields.body response
in
Rock.Middleware.create ~name:(Info.of_string "uppercaser") ~filter
let _ = App.empty
|> middleware uppercase
|> get "/hello" (fun req -> `String ("Hello World") |> respond')
|> App.cmd_name "Uppercaser"
|> App.command
|> Command.run
As you can tell, a middleware knows of 2 bits of information:
- ``handler : Request.t -> Response.t Deferred.t``. This is the
application request handler.
- ``req : Request.t``. The current request.
In our example, the middleware runs the handler and returns a response
with the uppercased body. But of course a general middleware doesn't
have to run handler at all, it can change the request before feeding the
handler, or it can simple add a logging message and let the handler
proceeed with the request.
You can tell that middleware is flexible, but to make it do something
more interesting, you must be able to store stuff along the
request/response. As I've mentioned before, the ``env`` bag is the
perfect place for that.
Here's another common use case. Suppose we'd like our webapp to
automatically authenticate users that provide their credentials using
the `HTTP
Basic `__
scheme. For example, we can export a export function like:
.. code-block:: ocaml
val user : Request.t -> user option
Here's how we could implement that:
.. code-block:: ocaml
open Core.Std
open Async.Std
open Opium.Std
type user = {
username: string;
(* ... *)
} with sexp
(* My convention is to stick the keys inside an Env sub module. By
not exposing this module in the mli we are preventing the user or other
middleware from meddling with our values by not using our interface *)
module Env = struct
let key : user Univ_map.Key.t = Univ_map.Key.create "user" <:sexp_of>
end
(*
Usually middleware gets its own module so the middleware constructor function
is usually shortened to m. For example, [Auth.m] is obvious enough.
The auth param (auth : username:string -> password:string -> user option)
would represent our database model. E.g. it would do some lookup in the db
and fetch the user.
*)
let m auth =
let filter handler req =
match req |> Request.headers |> Cohttp.Header.get_authorization with
| None ->
(* could redirect here, but we return user as an option type *)
handler req
| Some (Cohttp.Auth.Basic (username, password)) ->
match auth ~username ~password with
| None -> failwith "TODO: bad username/password pair"
| Some user -> (* we have a user. let's add him to req *)
let env = Univ_map.add_exn (Request.env req) Env.key user in
let req = Field.fset Request.Fields.env req env in
handler req
in
Rock.Middleware.create ~name:(Info.of_string "http basic auth") ~filter
let user req = Univ_map.find (Request.env req) Env.key
The middleware above might be basic as well but it should give you an
idea on how to create a wide variety of middleware. For example, serving
static pages, caching, throttling, routing, etc.
There are of course downsides however that I've yet to solve in Opium.
The main downside is that middleware does not commute. This means that
it must be executed in serial for every request. Much more worse however
is that middleware order will affect application behaviour. In fact, a
common source of bugs in Rack is executing middleware in the wrong
order. One obvious solution to this is to explictly specify dependencies
between middleware. Unfortunately it's pretty ugly and heavyweight and
it makes middleware less composable.
Final Note: After writing the first version of Opium I realized I was
copying the core of twitter's finnagle/finatra. I don't mention them
here however since finnagle is a lot more general and I don't know much
about it other than this excellent
`paper `__. I did end up borrowing
their terminology for the lower level plumbing though.
[1] [Rack Spec](https://github.com/rack/rack/blob/master/SPEC).
[2] [Ring Spec](https://github.com/mmcgrana/ring/blob/master/SPEC). A
huge improvement of Rack in my opinion. Rock could be thought of as a
typed Ring.
[3] If you're coming from the haskell WAI world I believe ``env`` would
be called the ``vault``. In fact, the way Rock is designed should be
very similar to the old WAI (< 3.0). Except that WAI use IO instead of
Deferred.t.
[4] [mixtbl](https://github.com/mjambon/mixtbl) An implementation
without core. It's also a hashtable instead of a map.
[5] [Haskell
Vault](http://apfelmus.nfshost.com/blog/2011/09/04-vault.html). Same
concept but implemented in the Haskell world.
[6] [Univeral Type](https://blogs.janestreet.com/rethinking-univ/). If
you'd like to dig deeper to see how the basic for such a map can be
implemented.