Stuck in the Middle: Adding Middleware to a Servant Server

middleware.png

This week we're going to take a quick look at a small problem. On a web server, the core of our functionality lies in the different endpoints. We define different behavior for different requests. Sometimes though, there are behaviors we want to add for all requests or all responses on our web server.

For these kinds of behaviors, we'll want to use "middleware". This week, we'll examine how we can represent middleware at a type level with the Network.Wai library. We'll see how it works in conjunction with a Servant server.

For a more in depth look at Haskell and web development, read our Haskell Web Series! You can also download our Production Checklist for some more ideas!

The Problem

As I was running some test code from a frontend client, I found certain requests returned network errors. This seemed odd, but after some research, I found the cause of the errors was the access control policy. Since this project was a pure debugging effort, I wanted to allow requests from any origin. But, this requires modifying every response that goes out from our server. How do we do this?

The answer is by attaching "middleware" to our application. This term will be familiar to those of you who do a lot of web programming in Javascript. Middleware is a layer between our server and incoming requests or outgoing responses. It can do something to change all the requests, or all the responses.

One common form of middleware on Javascript projects is the body-parser package. In Node.js applications, it makes it a lot easier to get information out of a request body. It acts on every request sent to your server, and parses out data from, say, a post request. Then it's much easier and intuitive to access that data. In our server, we'll put a layer between it and the outgoing responses to change the access policy.

Servant Refresher

For a moment, let's recall the basics of running a server using the Servant library. We'll specify an API with a single endpoint, a handler for that API, and we'll serve it:

type AppAPI = "api" :> "hello" :> Get ‘[JSON] String

appAPI :: Proxy AppAPI
appAPI = Proxy :: Proxy AppAPI

helloHandler :: Handler String
helloHandler = return "Hello World!"

apiHandler :: Server AppAPI
apiHandler = helloHandler

runServer :: IO ()
runServer = do
  let port = 8081
  run port (serve appAPI apiHandler)

To understand things a little better, let's examine the types from the Network.Wai library. The main output of all our Servant code is an Application. The serve function returns us this type. Then we can use run to run our application from a particular port.

serve :: Proxy api -> Server api -> Application

run :: Port -> Application -> IO ()

And it's also useful to know exactly what an Application is in the Wai library. This is actually a type synonym. Here is its real identity:

type Application = 
  Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived

This gives us an excellent functional definition of what a server application does. An application is a function taking two parameters. The first is an HTTP request. The second is a function from an HTTP response to a ResponseReceived item. We don't need to understand too much about this type. It indicates how the client has received our response. We'll use the function though to hijack the responses we return.

Adding Middleware

Above, we described middleware using some vague terminology. But now that we can see the types, it's easier to describe! Middleware is a function taking one application and returning another. The new application has slightly different behavior. This type alias exists within the Network.Wai library already:

type Middleware = Application -> Application

We will, of course, use the core functionality of the first application. But we'll apply something a little bit different to the response.

In our case, we'll want to allow all origins, so we'll add this field as a header to the response:

"Access-Control-Allow-Origin" : "*"

So let's start defining our middleware expression:

addAllOriginsMiddleware :: Application -> Application
addAllOriginsMiddleware = ...

And here's how we would add this middleware onto our server:

run port (addAllOriginsMiddleware (serve appAPI apiHandler))

Defining the Middleware

Now the type we've laid out for our middleware is pretty complicated. But we can start working through how we'll actually hook into the existing app. We'll take the base application as an argument, and then we want to return a function with two parameters:

addAllOriginsMiddleware :: Application -> Application
addAllOriginsMiddleware baseApp = \req responseFunc -> ...

Remember that baseApp is also a function taking a request and a response function. So we can partially apply it to our new application's request. We'll then have to define the new response function for the final app:

addAllOriginsMiddleware :: Application -> Application
addAllOriginsMiddleware baseApp = \req responseFunc -> 
  baseApp req newResponseFunc
    where
      newResponseFunc :: Response -> IO ResponseReceived
      newResponseFunc = ...

We want to take the existing response from the base application and change it a little bit. So let's define a function to do this:

addOriginsAllowed :: Response -> Response
addOriginsAllowed = ...

We can now hook this function into our middleware using function composition! Remember how we can use the composition operator in this circumstance:

-- Function composition, on our types:
(.) :: 
  (Response -> IO ResponseReceived) ->
  (Response -> Response) ->
  (Response -> IO ResponseReceived)

addAllOriginsMiddleware :: Application -> Application
addAllOriginsMiddleware baseApp = \req responseFunc -> baseApp req newResponseFunc
  where
    newResponseFunc :: Response -> IO ResponseReceived
    newResponseFunc = responseFunc . addOriginsAllowed

And we're almost done!

Modifying the Header

All we have to do now is handle the dirty work of appending our new header to the response. We'll use the mapResponseHeaders function. It treats our headers as a list, and applies a function to change that list. All we do is append our new key/value pair!

addOriginsAllowed :: Response -> Response
addOriginsAllowed = mapResponseHeaders $
  (:) ("Access-Control-Allow-Origin", "*")

Now our application modifies every header! We're done!

Conclusion

This week we went into the guts of Haskell's HTTP libraries a little bit. We used a type driven development approach to define what middleware is for a web server. Then we saw how to add it to our application. We saw how to add a new header to every response going out from our server.

For a more in depth tutorial on how to use Servant and combine it with other services, read our Haskell Web Series! To learn about more libraries you can use in your apps, download our Production Checklist!