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?
Middleware is a design pattern to eloquently add cross cutting concerns like logging, handling authentication, or gzip compression without having many code contact points.
Now, we need to add our custom middleware in the request pipeline by using Use extension method as shown below. We can add middleware using app. UseMiddleware<MyMiddleware>() method of IApplicationBuilder also. Thus, we can add custom middleware in the ASP.NET Core application.
A "middleware" is a function that works with every request before it is processed by any specific path operation. And also with every response before returning it. It takes each request that comes to your application. It can then do something to that request or run any needed code.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With