Handlers and Middlewares
Now that you've learned how routing works, it is time to learn about request handlers.
A typical web application will extract some data from the request, do some processing, and then produce a response. WebGear request handlers help to implement all these three steps in a convenient, type-safe manner. Let us see how.
Request Handlers
A request handler is an arrow that takes a request as input and produces a response as output. You already saw a few handlers in this tutorial. They have a few properties that you should learn to use WebGear effectively.
Request handlers represent an abstract computation that can derive a response based on a request. This abstract
computation can be converted to a concrete web application by passing it to the toApplication
function. You
already saw the usage of toApplication
in previous chapters, but we never mentioned that its argument is a
handler.
Request handlers are composable. In the routing chapter, you saw that multiple
endpoints can be combined with the <+>
operator to produce a single handler.
Middlewares
A middleware is a function that takes a handler as input and produces another handler as output. You've encountered a
few middlewares already even though they weren't explicitly labelled so. The method
, path
, pathVar
,
and pathEnd
functions from the routing are examples of middlewares. They accept a handler as
input and invokes it only if some criteria is met.
Since middlewares return a handler, we can chain them together and produce a handler as shown in the example below:
Traits
WebGear has many more middlewares, we'll explore them in the next chapter. For now, let us turn our attention to the
pathVar
middleware. In the code above, pathVar @"userId" @Int
extracts a path variable of type Int
. How can we use this variable in our handler?
method HTTP.GET $ path "/api/user" $ pathVar @"userId" @Int $ pathEnd $
proc request -> do
let userId :: Int
userId = pick @(PathVar "userId" Int) $ from request
respondA HTTP.ok200 PlainText -< "ID = " <> show userId
You just pick the value from the request. But how do we know that the request contains such a path variable? After
all, the request can contain a completely different path. Note that the type of userId
is Int
. Shouldn't
it be Maybe Int
to accommodate the case where the request does not have the path variable?
Let us experiment a little bit; what if you remove the pathVar
middleware from the code?
method HTTP.GET $ path "/api/user" $ pathEnd $
proc request -> do
let userId :: Int
userId = pick @(PathVar "userId" Int) $ from request
respondA HTTP.ok200 PlainText -< "ID = " <> show userId
Try compiling this code and you'll get an error:
• The value doesn't have the ‘PathVar "userId" Int’ trait.
Did you forget to apply an appropriate middleware?
...
or did you use a wrong trait type?
...
That's very impressive. WebGear stops us from using an attribute that could be missing from the request. We can't pick the path variable from the request unless we have used the appropriate middleware. How does that work? What is this trait mentioned in the error message?
It'll be clearer if we add a type signature to the handler:
myApp :: Wai.Application
myApp = toApplication $
method HTTP.GET $ path "/api/user" $ pathVar @"userId" @Int $ pathEnd userHandler
userHandler :: HasTrait (PathVar "userId" Int) ts => ServerHandler IO (Request `With` ts) Response
userHandler = proc request -> do
let userId :: Int
userId = pick @(PathVar "userId" Int) $ from request
respondA HTTP.ok200 PlainText -< "ID = " <> show userId
The type of userHandler
is ServerHandler IO (Request
Withts) Response
. Here, ServerHandler IO
is the
type of the arrow used as the handler. The arrow input is Request `With` ts
and the output is Response
.
In this type, ts
is a list of trait types. You might be wondering, what is a trait? Simply put, traits are
attributes associated with a request. PathVar "userId" Int
is one such trait that represents a path variable. The
ts
type variable contains all traits that are known to be present in the request. The HasTrait (PathVar"userId" Int) ts
constraint is an assertion that the path variable trait definitely exists in the list ts
. That
is why we can use pick @(PathVar "userId" Int)
to get a user ID without having to handle the case of a missing
path variable.
In order to satisfy the HasTrait
constraint, we must use the pathVar
middleware. If you omit that
middleware, the constraint will not be satisfied and GHC will show an error message as shown above.
In the rest of this tutorial, you will see this pattern used again and again. Your handler will be wrapped in an
appropriate middleware and then the handler can access trait using the pick
function.
A Better Type Signature
The type signature of userHandler
given above works fine. However, WebGear handlers tend to use a more general type:
RequestHandler h ts
is a type synonym and is equivalent to h (Request `With` ts) Response
. Doesn't that
look very similar to ServerHandler IO (Request `With` ts) Response
? Of course, it does. The main change we did
is to replace ServerHandler IO
with a type variable h
.
As mentioned above, ServerHandler IO
is the type of the arrow used by userHandler
. This arrow allows us to
convert the handler to a Wai application using the toApplication
function. But WebGear handlers are not limited
to just being an application. In a later chapter, you'll learn how to generate OpenAPI or Swagger documentation from
these handlers. In order to support all such use cases, we switch to a polymorphic type variable h
instead of the
concrete arrow type ServerHandler IO
. WebGear can substitute an appropriate type in place of h
depending
on the use case.
As a result of this change, we need an additional constraint StdHandler h m
in the type signature. This
constraint enforces that h
is a handler arrow and has capabilities that all WebGear handlers are required to
have. For example, handler arrows can embed monadic actions in them. We'll see more on that in the next section.
Going forward, we will use this polymorphic type signature for all our handlers.
Monadic Actions in Handlers
Most WebGear handlers will need some monadic actions to process the request. As an example, let us consider a handler
that returns the current date and time in UTC. We can use the Data.Time.getCurrentTime
function for this
pupose. But, that function has the type IO UTCTime
. How do we add it to a handler which is an arrow and not a
monad?
It is fairly straightforward. All handlers allow embedding monadic actions in them using a function arrM
. This is
how you'd use it:
import Data.Time
import Control.Monad.IO.Class
currentTimeHandler ::
forall h m ts.
(StdHandler h m, MonadIO m) =>
RequestHandler h ts
currentTimeHandler = proc request -> do
now <- arrM getNow -< ()
respondA HTTP.ok200 PlainText -< show now
where
getNow :: () -> m UTCTime
getNow () = liftIO getCurrentTime
If you have a function of type a -> m b
where m
is a monad, the function arrM
can lift it to a
handler arrow of type StdHandler h m => h a b
. In the above example, we lifted the getNow
function to an
arrow of type h () UTCTime
and used it inside the proc
body.
Unlike monads, handlers (and arrows in general) require both an input and an output. The getCurrentTime
IO action
does not require any inputs and we should convert it to a function by passing ()
as an input.
Bring Your Own Monad
Notice how we used the MonadIO m
constraint instead of using the concrete IO
type. Using a polymorphic
type helps us to switch to a different monad if required without modifying the handler.
You may use any monad of your choice with request handlers. However, the toApplication
function requires the
handler to be based on the #hs IO
monad. So, how do you convert handlers based on your custom monad stack to a Wai
application?
The answer is to use the transform
function to convert your monad stack. Let us assume you have a ReaderT
stack. This is how you generate a Wai application in
that case:
apiHandler :: StdHandler h (ReaderT Env IO) => RequestHandler h ts
apiHandler = ...
application :: Env -> Wai.Application
application env = toApplication $ transform appToIO apiHandler
where
appToIO :: ReaderT Env a -> IO a
appToIO f = runReaderT f env
The transform
function has the following type:
Given a function - such as appToIO
above - that transforms one monadic action to another, it can transform
between the corresponding ServerHandler
types. You then pass the transformed handler to toApplication
.
Summary
In this chapter, you learned the basics of request handlers, middlewares, and traits. By now, you have a good understanding of how traits help in accessing request attributes in a type safe manner. You also learned to write polymorphic type signatures for request handlers.
In the next chapter we'll meet many more commonly used middlewares and traits.