Requests are handled by executing a stack of middlewares!

Middlewares

A set of functions that either respond to an http request or pass it on to the next middleware on the stack.

For this guide we will be writing two example middlewares. One to log every request and how long it takes and another which will guard a section of the website exclusive to admin users.

# What are Middlewares?

Middlewares have a wide variety of uses. They are executed in the order in which they are registered so be sure to keep that in mind. Check out the Middleware module for the spec:

1// Middleware module
2type t('sessionData) = (RequestHandler.t('sessionData), Route.t, Req.t('sessionData), Res.t) => Lwt.t(Res.t)
1(* Middleware module *)
2type 'sessionData t = 'sessionData RequestHandler.t -> Route.t -> 'sessionData Req.t -> Res.t -> Res.t Lwt.t

Middlewares can either handle the http request/response lifecycle themselves or call the passed in request handler (which is the next middleware in the stack) passing the route, req, and res. Once the list of middlewares has been exhausted it will then be passed on to the main request handler.

# Authorization Example

We can easily create a middleware function to protect certain endpoints of our application. All middlware functions will be executed before the requestHandler, making it easy to stop the request/response lifecycle if needed.

1// ...
2let serverConf: Naboris.ServerConfig.t(userData) = Naboris.ServerConfig.create()
3  |> Naboris.ServerConfig.addMiddleware((next, route, req, res) => switch (Naboris.Route.path(route)) {
4    | ["admin", ..._] => switch (Naboris.Req.getSessionData(req)) {
5      | Some({ is_admin: true, ..._}) => next(route, req, res)
6      | _ =>
7        res
8          |> Naboris.Res.status(401)
9          |> Naboris.Res.text(req, "Unauthorized");
10      }
11    | _ => next(route, req, res)
12  });
13// ...
1(* ... *)
2let server_conf: user_data Naboris.ServerConfig.t = Naboris.ServerConfig.create ()
3  |> Naboris.ServerConfig.addMiddleware (fun next route req res ->
4    match (Naboris.Route.path route) with
5      | "admin" :: _ ->
6        (match (Naboris.Req.getSessionData req) with
7          | Some({ is_admin = true; _}) -> next route req res
8          | _ ->
9            res
10              |> Naboris.Res.status 401
11              |> Naboris.Res.text req "Unauthorized")
12      | _ -> next route req res)
13(* ... *)

The above middleware example will check the session data for an is_admin flag on any route starting with /admin/, if the flag is present next is called and the request is handled as normal. If the flag is not present the middleware completes the request/response lifecycle by sending a 401 - Unauthorized response.

# Logger Example

Middlewares can also execute code after the next function is called. This will be done in the next example which will log all incoming requests and how long it took for them to finish.

1// ...
2let serverConf: Naboris.ServerConfig.t(userData) = Naboris.ServerConfig.create()
3  |> Naboris.ServerConfig.addMiddleware((next, route, req, res) => {
4    let startTime = Unix.gettimeofday();
5    let path = String.concat("", Naboris.Route.path(route)) ++ Naboris.Route.rawQuery(route);
6    print_endline("Start Serving - " ++ path);
7    Lwt.bind(() => next(route, req, res), (servedResponse) => {
8      // this code is executed after next() resolves
9      let endTime = Unix.gettimeofday();
10      let ms = (endTime -. startTime) *. 1000);
11      print_endline(path ++ " - " ++ string_of_int(Res.status(servedResponse)) ++ " - in " ++ string_of_float(ms) ++ "ms");
12      Lwt.return(servedResponse);
13    });
14  });
15// ...
1(* ... *)
2let server_conf: user_data Naboris.ServerConfig.t = Naboris.ServerConfig.create ()
3  |> Naboris.ServerConfig.addMiddleware (fun next route req res ->
4    let start_time = Unix.gettimeofday () in
5    let path = (String.concat "" (Naboris.Route.path route)) ^ (Naboris.Route.rawQuery route) in
6    let _ = print_endline ("Start Serving - " ^ path) in
7    Lwt.bind(fun () -> next(route, req, res), fun servedResponse ->
8      (* this code is executed after next() resolves *)
9      let end_time = Unix.gettimeofday () in
10      let ms = (end_time -. start_time) *. 1000 in
11      let _ = print_endline (path ^ " - " ^ (string_of_int (Res.status servedResponse)) ^ " - in " ^ string_of_float(ms) ^ "ms") in
12      Lwt.return servedResponse))
13(* ... *)

# Combining Middlewares

As stated above middlewares are executed in the order in which they are registered. Given the two examples above let's see how they can be used in combination.

1// ...
2let serverConf: Naboris.ServerConfig.t(userData) = Naboris.ServerConfig.create()
3  |> Naboris.ServerConfig.addMiddleware(authorizationMiddleware)
4  |> Naboris.ServerConfig.addMiddleware(loggerMiddleware);
5// ...
1(* ... *)
2let server_conf: user_data Naboris.ServerConfig.t = Naboris.ServerConfig.create ()
3  |> Naboris.ServerConfig.addMiddleware authorization_middleware
4  |> Naboris.ServerConfig.addMiddleware logger_middleware in
5(* ... *)

This will work fine but there is a problem. If a non-admin user tries to reach a /admin/ endpoint the request won't be logged. This is because the authorizationMiddleware will finish the lifecycle of the request without ever calling the next midddlware.

Easy to solve:

1// ...
2let serverConf: Naboris.ServerConfig.t(userData) = Naboris.ServerConfig.create()
3  |> Naboris.ServerConfig.addMiddleware(loggerMiddleware)
4  |> Naboris.ServerConfig.addMiddleware(authorizationMiddleware);
5// ...
1(* ... *)
2let server_conf: user_data Naboris.ServerConfig.t = Naboris.ServerConfig.create ()
3  |> Naboris.ServerConfig.addMiddleware logger_middleware
4  |> Naboris.ServerConfig.addMiddleware authorization_middleware in
5(* ... *)

Now all requests will be logged. Even the 401 - Unauthorized requests.

Support us on GitHub

Star, follow, fork

Star Fork

Found a typo? a bug? or something that just doesn't make any sense? Help improve these docs by opening a github issue.

naboris source code is licensed MIT.
It can be used, copied, and modified free of charge. However, the software is provided "as is" without any warranties. Click the link above for more information.