11  Middleware

Middlewares are functions that run before anything in the application, and are mostly used for request pre-processing.

Let’s see an example application that uses middleware:

library(ambiorix)

#' Middleware to print current date & time
#'
#' @param req The request object.
#' @param res The response object.
now <- function(req, res) {
  print(Sys.time())
}

home_get <- function(req, res) {
  res$send("Using {ambiorix}!")
}

about_get <- function(req, res) {
  res$text("About")
}

app <- Ambiorix$new(port = 3000L)

app$use(now)
app$get("/", home_get)
app$get("/about", about_get)

app$start()
  1. Just like any request handler in Ambiorix, a middleware must take the request and response objects.
  2. To register the middleware, pass it to the use() method of the app.

Go ahead and run the app. Observe the output on your console when you visit either of the two routes (/ & /about).

You will notice that the current date & time is printed to the console each time a request is made.

Now stop the app and modify home_get() as follows:

home_get <- function(req, res) {
  print("In home_get()")
  res$send("Using {ambiorix}")
}

Re-run the app and visit /. What do you observe? Which print statement is made first?

That should be enough to show you that middlewares indeed run before anything else in the application.

Why must a middleware take the request and response objects, you ask?

11.1 Request pre-processing

There is no better way to explain this than by using an example.

library(ambiorix)
library(htmltools)

#' Auth middleware
#'
#' @param req The request object.
#' @param res The response object.
auth <- function(req, res) {
  user <- list(
    first_name = "Kennedy",
    last_name = "Mwavu",
    email = "mwavu@mail.com"
  )
  req$user <- user
}

home_get <- function(req, res) {
  # get 'user' from the request object:
  user_details <- req$user
  html <- tags$div(
    tags$h3("User details:"),
    tags$ul(
      tags$li("First name:", user_details$first_name),
      tags$li("Last name:", user_details$last_name)
    ),
    tags$p(
      "To see the user email, visit",
      tags$a(
        href = "/about",
        "the about page"
      )
    )
  )
  res$send(html)
}

about_get <- function(req, res) {
  user_details <- req$user
  html <- tags$div(
    tags$h3(user_details$first_name, user_details$last_name, ":"),
    tags$ul(
      tags$li("Email:", user_details$email)
    )
  )
  res$send(html)
}

app <- Ambiorix$new(port = 3000L)

app$use(auth)
app$get("/", home_get)
app$get("/about", about_get)

app$start()

When our auth middleware runs, it adds the object user to request object. Any other handler can then access the user object using req$user.

Be careful not to unknowingly overwrite existing fields in the request object.

11.2 Returning from a middleware

Sometimes, when a request lacks something you need or the client making the request is not authorized, you can opt for a middleware which checks for that and returns early before the request hits any other handler.

Using the same example as above, modify the auth middleware to now be this:

#' Auth middleware
#'
#' @param req The request object.
#' @param res The response object.
auth <- function(req, res) {
  user <- list(
    first_name = "Kennedy",
    last_name = "Mwavu",
    email = "mwavu@mail.com",
    status = "disabled"
  )

  if (!identical(user$status, "enabled")) {
    return(
      res$set_status(401L)$send("Unauthorized!")
    )
  }

  req$user <- user
}

Start the app and try visiting any of the endpoints defined earlier. What happens if you toggle the user status to “enabled”?

If you have noticed, a middleware behaves more or less like a normal function:

  • It takes parameters (req, res)
  • Has return value

When its return value is a valid response object, the application immediately returns that to the client, without any further handling.

11.3 Specific routes

So far, all the middlewares we’ve used run on every single request, regardless of the route. But what if you only want a middleware to run on a specific endpoint?

You can do this by checking req$PATH_INFO inside the middleware. If the path doesn’t match, call forward() to skip the middleware and let the request continue to the next handler.

library(ambiorix)
library(htmltools)

#' Middleware that only runs on '/about'
#'
#' @param req The request object.
#' @param res The response object.
about_middleware <- function(req, res) {
  is_about <- identical(req$PATH_INFO, "/about")

  if (!is_about) {
1    return(forward())
  }

2  cat("Viewing the about section...\n")
}

home_get <- function(req, res) {
  html <- tags$h3("hello! welcome home.")
  res$send(html)
}

about_get <- function(req, res) {
  html <- tags$h3("learn more about us")
  res$send(html)
}

app <- Ambiorix$new(port = 3000L)

app$use(about_middleware)
app$get("/", home_get)
app$get("/about", about_get)

app$start()
1
forward() tells Ambiorix to skip this middleware and move on to the next handler.
2
This line only executes for requests to /about.

Run the app and visit both / and /about. Watch your console. You will see that “Viewing the about section…” only appears when you visit /about.

11.3.1 A note on forward()

forward() is not specific to middleware. It can be used in any handler to tell Ambiorix to skip the current handler and try the next matching one.

library(ambiorix)

app <- Ambiorix$new()

app$get("/next", function(req, res) {
  forward()
})

app$get("/next", function(req, res) {
  res$send("Hello from the second handler!")
})

app$start(port = 3000L)

Visit /next. The first handler skips itself, and the second handler sends the response.

Returning NULL or anything that isn’t a valid response object from a middleware function has the same effect, but forward() is more explicit.

Explicit is better than implicit. – The Zen of Python

11.4 Specific Methods

In the same way you can target specific routes, you can also target specific HTTP methods. The req$REQUEST_METHOD field tells you whether the request is a GET, POST, PUT, DELETE, etc.

Here’s a middleware that only runs on POST requests:

library(ambiorix)

#' Middleware that logs POST requests
#'
#' @param req The request object.
#' @param res The response object.
log_post <- function(req, res) {
  if (!identical(req$REQUEST_METHOD, "POST")) {
    return(forward())
  }

  now <- format(Sys.time(), format = "%F %T")
  cat("[POST]", now, req$PATH_INFO, "\n")
}

home_get <- function(req, res) {
  res$send("Home page (GET)")
}

home_post <- function(req, res) {
  res$send("Form submitted (POST)")
}

app <- Ambiorix$new()

app$use(log_post)
app$get("/", home_get)
app$post("/", home_post)

app$start(port = 3000L)

Visit / in your browser – no log on the console. Now send a POST request using curl:

curl -X POST http://127.0.0.1:3000/

You will see the log message appear on the console.

Of course, you can combine both checks: target a specific method and a specific route in the same middleware. It’s just regular R code.

11.5 Order matters

Middlewares are executed in the order they are registered with use().

This is a simple but important rule. If middleware A depends on something set by middleware B, then B must be registered first.

library(ambiorix)

app <- Ambiorix$new()

app$use(function(req, res) {
  req$x <- 1
  cat("Middleware 1: set x to", req$x, "\n")
})

app$use(function(req, res) {
  req$x <- req$x + 10
  cat("Middleware 2: x is now", req$x, "\n")
})

app$get("/", function(req, res) {
  res$send(paste("x is", req$x))
})

app$start(port = 3000L)

Run the app and visit /. Your console will show:

Middleware 1: set x to 1
Middleware 2: x is now 11

And the browser shows: “x is 11”.

Now swap the order of registration:

app$use(function(req, res) {
  req$x <- req$x + 10
  cat("Middleware 2: x is now", req$x, "\n")
})

app$use(function(req, res) {
  req$x <- 1
  cat("Middleware 1: set x to", req$x, "\n")
})

Visit / again. The result is different because the first middleware tries to add 10 to req$x before it has been set.

The takeaway: if you have a middleware that other middlewares or handlers depend on, register it first.

This is especially relevant for things like CORS and authentication, which we’ll look at next.

11.6 CORS

Cross-Origin Resource Sharing (CORS) is a mechanism that allows a server to indicate which origins are permitted to load its resources.

Say you have a frontend application running on http://127.0.0.1:8000 and your Ambiorix API on http://127.0.0.1:3000. When the frontend tries to fetch data from the API, the browser blocks the request because the two are on different origins (different ports count).

CORS headers tell the browser: “it’s okay, let this request through.”

A CORS middleware sets these headers on every response.

library(ambiorix)

#' CORS middleware
#'
#' @details
#' Sets these headers in the response:
#' - `Access-Control-Allow-Origin`
#' - `Access-Control-Allow-Methods`
#' - `Access-Control-Allow-Headers`
#'
#' @param req The request object.
#' @param res The response object.
cors <- function(req, res) {
  res$header(
    "Access-Control-Allow-Origin",
1    "http://127.0.0.1:8000"
  )

  if (identical(req$REQUEST_METHOD, "OPTIONS")) {
2
    res$header(
      "Access-Control-Allow-Methods",
      "GET, POST, PUT, DELETE"
    )
    res$header(
      "Access-Control-Allow-Headers",
      req$HEADERS$`access-control-request-headers`
    )

    return(
      res$send("")
    )
  }
}

home_get <- function(req, res) {
  res$json(list(msg = "Hello from the API!"))
}

app <- Ambiorix$new()

app$use(cors)
app$get("/", home_get)

app$start(port = 3000L)
1
Set this to the origin of your frontend application. Do not include a trailing slash. http://127.0.0.1:8000/ will not work.
2
Before making the actual request, browsers send a “preflight” OPTIONS request to check whether the server allows the cross-origin request. This block handles that preflight.

11.6.1 Testing it

To see CORS in action, you need a frontend making a request from a different origin.

Once the application above is running, create a file called index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>CORS Test</title>
  </head>
  <body>
    <h3>CORS Test</h3>
    <pre id="result">Loading...</pre>

    <script>
      fetch("http://127.0.0.1:3000/")
        .then((res) => res.json())
        .then((data) => {
          document.getElementById("result").textContent = JSON.stringify(
            data,
            null,
            2,
          );
        })
        .catch((err) => {
          document.getElementById("result").textContent =
            "Error: " + err.message;
        });
    </script>
  </body>
</html>

Then serve it from a different port:

library(ambiorix)

app <- Ambiorix$new(port = 8000L)

app$get("/", function(req, res) {
  res$send_file("index.html")
})

app$start()

Now open http://127.0.0.1:8000/index.html in your browser. You should see the JSON response from your Ambiorix API displayed on the page.

If you remove the CORS middleware from the app, restart, and refresh the page, the browser will block the request and you will see an error in the browser console.

11.6.2 Notes

  • Change the allowed origin to match your actual frontend URL.
  • If your API uses cookies or authentication tokens across origins, you will also need to set the Access-Control-Allow-Credentials header to "true" inside the OPTIONS block.
  • Because middlewares run in order, make sure the CORS middleware is registered before any other middleware or route.

11.7 Auth

A good use-case of middleware is protecting certain routes in your application and requiring the client to be authenticated and/or authorized to access those routes.

Earlier in this chapter, we saw a simple auth middleware that attaches a user to the request. Let’s build on that idea into something more realistic.

11.7.2 Protecting a group of routes

When you have many routes that need protection, repeating the check in every handler is tedious. Instead, use a Router.

A middleware registered on a router only runs for routes defined on that router.

library(ambiorix)
library(htmltools)

# --- auth middleware --------------------------------------------------------

auth <- function(req, res) {
  token <- req$cookie$auth_token

  if (is.null(token)) {
    res$status <- 401L

    return(
      res$json(
        list(error = "Unauthorized")
      )
    )
  }

  req$user <- list(
    name = "Kennedy",
    role = "admin"
  )
}

# --- protected router -------------------------------------------------------

protected <- Router$new("/app")

protected$use(auth)

protected$get("/dashboard", function(req, res) {
  html <- tags$div(
    tags$h3("Dashboard"),
    tags$p(paste("Hello,", req$user$name))
  )
  res$send(html)
})

protected$get("/settings", function(req, res) {
  html <- tags$div(
    tags$h3("Settings"),
    tags$p(paste("Role:", req$user$role))
  )
  res$send(html)
})

# --- public routes ----------------------------------------------------------

home_get <- function(req, res) {
  html <- tags$div(
    tags$h3("Public Home"),
    tags$a(href = "/app/dashboard", "Dashboard"),
    tags$br(),
    tags$a(href = "/app/settings", "Settings")
  )
  res$send(html)
}

# --- app --------------------------------------------------------------------

app <- Ambiorix$new()

app$get("/", home_get)
app$use(protected)

app$start(port = 3000L)

/ is public. /app/dashboard and /app/settings are protected by the auth middleware.

Any new route you add to the protected router automatically gets the auth check, without you having to remember to add it.