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.
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?
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
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
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.
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 -> ...
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!
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!