Search code examples
haskellclojurehaskell-wai

Middleware for per-request data


In clojure, I can write something like this:

(defn wrap-my-header
  [handler]
  (fn [request]
    (let [request (if (get-in request [:headers "my-header"])
                    (assoc request :has-my-header? true)
                    request)]
      (handler request))))

In this middleware, I'm checking if I have a non-nil value in my-header in :headers, if yes, I'll attach some data in the request map. This demonstrates that I can treat request and response as a somewhat "stateful" data.

I'm still new in haskell and wanted to do similar things with scotty. After looking at the type of middleware, I can create a middleware like this:

myMiddleware :: Middleware 
myMiddleware app req respond = app req respond

After staring at the type for a long time, I still don't know how to do it. Some reading and thinking makes me assume that this is not possible, Middleware can only only short-circuit the handler and/or alter the generated response. Is this true?



Solution

  • This confused me for a long time too! But figuring it out gave me a helpful technique for understanding Haskell library types.

    First I'll start with my middleware being undefined:

    myMiddleware :: Middleware
    myMiddleware = undefined
    

    So what is Middleware? The key is to look at the definition of the type:

    type Middleware = Application -> Application
    

    Let's start at the first layer (or level of abstraction) by having the middleware take an Application and return an Application. We don't know how to modify an application, so we'll return exactly what's passed in for now.

    myMiddleware :: Application -> Application
    myMiddleware theOriginalApp = theOriginalApp
    

    But what is an Application? Again, let's turn to Hackage:

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

    An Application is a function! We may not know exactly what each part is supposed to do or be, but we can find out. Let's replace Application in our type signature with the function type:

    myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived) 
                 -> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
    myMiddleware theOriginalApp = theOriginalApp
    

    Now we can see this type should allow us to access a Request! But how do we use it?

    We can expand theOriginalApp in the function definition into a lambda expression that matches the return type:

    myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived) 
                 -> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
    myMiddleware theOriginalApp = (\req sendResponse -> undefined)
    

    We can do whatever we want with the request now:

    myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived) 
                 -> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
    myMiddleware theOriginalApp = (\req sendResponse ->
      let myModifiedRequest = addSomeHeadersIfMissing req in
        undefined)
    

    Now what about that undefined? Well, we're trying to match our lambda to the type of that return function, which takes a Request and a function (that we don't care about) and returns an IO ResponseReceived.

    So, we need something that can use myModifiedRequest and return an IO ResponseReceived. Luckily our type signature indicates that theOriginalApp has the right type! To make it fit, we only need to give it the sendResponse function too.

    myMiddleware :: (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived) 
                 -> (Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived)
    myMiddleware theOriginalApp = (\req sendResponse ->
      let myModifiedRequest = addSomeHeadersIfMissing req in
        theOriginalApp myModifiedRequest sendResponse)
    

    And that's it, that will work! We can improve the readability by simplifying the type annotation back to Middleware, and getting rid of the lambda. (We can also eta-reduce and remove the sendResponse term from both the arguments and the definition, but I think it's clearer if it stays.)

    The result:

    myMiddleware :: Middleware
    myMiddleware theOriginalApp req sendResponse =
      let myModifiedRequest = addSomeHeadersIfMissing req in
        theOriginalApp myModifiedRequest sendResponse