Request Traits
In the last chapter, you learned how to extract a path variable from a request using a middleware. WebGear includes many more traits and middlewares for extracting information from requests. Most of these traits follow a similar pattern. Their usage looks like this:
middleware errorHandler $
proc request -> do
let attribute :: TheAttribute = pick @TheTrait $ from request
...
As you can see, the trait supports a middleware that you wrap your handler with. Some traits support more than one middleware and you need to select a suitable one.
Most of the middlewares accept an error handler as the first argument. This handler deals with situations when the middleware encounters an error and needs to return some kind of an error response.
After you wrap the handler with a middleware, you may use the pick
function to extract the trait attribute
associated with the request. Note that there are two types involved here. TheTrait
is the type of the trait and
TheAttribute
is the type of the associated attribute value. In the path variable example from the previous
chapter, these types would be PathVar "userId" Int
and Int
respectively.
In summary, if you know the suitable middleware, the trait type, and the attribute type, you can extract that attribute from a request in a type-safe and straightforward manner.
Now that you've seen the general pattern of using request traits, what follows is an in-depth look at the traits and middlewares included in WebGear.
Query Parameters
You can extract query parameters from the request and parse the string value into a Haskell type of your choice.
To illustrate this, consider a variation of the currentTimeHandler
from the previous chapter. This time our
handler will accept a query parameter named diff
which must be an integer representing a length of time in
seconds. It will then respond with a timestamp calculated by adding this length of time to the current UTC time.
Here is how you could implement this:
timeHandler ::
forall h m ts.
(StdHandler h m, MonadIO m, Get h (RequiredQueryParam "diff" Integer)) =>
RequestHandler h ts
timeHandler =
queryParam @"diff" @Integer errorHandler $
proc request -> do
let seconds :: Integer = pick @(RequiredQueryParam "diff" Integer) $ from request
time <- arrM getTimeDiff -< fromInteger seconds
respondA HTTP.ok200 PlainText -< show time
errorHandler :: StdHandler h m => h (Request `With` ts, Either ParamNotFound ParamParseError) Response
errorHandler = proc (_, e) ->
case e of
Left ParamNotFound ->
respondA HTTP.badRequest400 PlainText -< "Missing query parameter: diff"
Right (ParamParseError msg) ->
respondA HTTP.badRequest400 PlainText -< "Invalid query parameter: diff: " <> msg
getTimeDiff :: MonadIO m => NominalDiffTime -> m UTCTime
getTimeDiff diff = addUTCTime diff <$> liftIO getCurrentTime
Let us examine the important parts of this code.
The new constraint Get h (RequiredQueryParam "diff" Integer)
is added to indicate that the handler arrow h
must be capable of retrieving the query parameter trait from the request. Then, we can use the queryParam @"diff"@Integer
middleware to ensure that the request contains the parameter. After that, we use pick@(RequiredQueryParam "diff" Integer)
to extract the parameter value as an integer.
Finally, pay close attention to the type of errorHandler
. It's input is a pair containing the original request
and an error object. There are two possible types of error - either the parameter is missing or it cannot be parsed as
an integer. These two cases are represented using the type Either ParamNotFound ParamParseError
.
WebGear has three more middlewares related to query parameters. They are listed below:
Middleware | Trait Type | Attribute Type |
---|---|---|
optionalQueryParam |
QueryParam Optional Strict name val or OptionalQueryParam name val |
Maybe val |
lenientQueryParam |
QueryParam Required Lenient name val |
Either Text val |
optionalLenientQueryParam |
QueryParam Optional Lenient name val |
Maybe (Either Text val) |
Their usage is very similar to queryParam
. The Optional
variants can be used when the query parameter is not
mandatory and the Lenient
variants can be used when you don't want to fail in case of parsing errors. Note that the
trait and attribute types change accordingly.
Headers
Request headers can be accessed very similar to query parameters in WebGear. These are the middlewares and associated types for request headers:
Middleware | Trait Type | Attribute Type |
---|---|---|
header |
RequestHeader Required Strict name val or RequiredRequestHeader name val |
Maybe val |
optionalHeader |
RequestHeader Optional Strict name val or OptionalRequestHeader name val |
Maybe val |
lenientHeader |
RequestHeader Required Lenient name val |
Either Text val |
optionalLenientHeader |
RequestHeader Optional Lenient name val |
Maybe (Either Text val) |
Since they are very similar to query parameters, we'll omit an example here. You may refer to the API documentation for more details.
Cookies
There are two middlewares for access request cookies:
Middleware | Trait Type | Attribute Type |
---|---|---|
cookie |
Cookie Required name val |
val |
optionalCookie |
Cookie Optional name val |
Maybe val |
As you've probably guessed, the former is used in case of mandatory cookies and the latter in case of optional cookies.
As an example, the code below accesses a mandatory cookie named session-id
from the request.
sessionHandler :: (StdHandler h m, Get h (Cookie Required "session-id" Text)) => RequestHandler h ts
sessionHandler =
cookie @"session-id" @Text errorHandler $
proc request -> do
let sessionId = pick @(Cookie Required "session-id" Text) $ from request
respondA HTTP.ok200 PlainText -< "Session ID: " <> sessionId
But this raises a compilation error:
• Could not deduce ‘HasTrait
(RequestHeader Required Strict "Cookie" Text) ts’
arising from a use of ‘cookie’
from the context: (StdHandler h m,
Get h (Cookie Required "session-id" Text))
This is because extracting a cookie is a two step process. First, you need to get the Cookie
header from the request
and then extract a cookie from the header value. WebGear models such dependencies using trait prerequisites. In this
case, the RequestHeader Required Strict "Cookie" Text
trait is a prerequisite for the Cookie Required"session-id" Text
trait.
To satisfy the prerequisite, we make a couple of changes:
sessionHandler ::
( StdHandler h m
, Gets h [RequiredRequestHeader "Cookie" Text, Cookie Required "session-id" Text]
) => RequestHandler h ts
sessionHandler =
header @"Cookie" @Text errorHandler $
cookie @"session-id" @Text errorHandler $
proc request -> do
let sessionId = pick @(Cookie Required "session-id" Text) $ from request
respondA HTTP.ok200 PlainText -< "Session ID: " <> sessionId
The header @"Cookie" @Text
middleware will satisfy the prerequisite. But you also need to add a Get
constraint for this new trait to the type signature. In this case, we used the Gets
constraint which supports a
list of traits. This is a concise form as compared to adding one constraint for each trait.
Request Body
The requestBody
middleware is used to extract the HTTP body from a request as a Haskell value. It accepts two
arguments - a MIME type that determines how the body is converted to the Haskell value and an error handler for
processing errors.
As an example, let us look at how to parse a JSON formatted request body into a Haskell type:
data MyRecord = MyRecord
{ -- some JSON fields go here
}
deriving stock (Generic, Show)
deriving anyclass (FromJSON)
jsonBodyHandler :: (StdHandler h m, Get h (Body JSON MyRecord)) => RequestHandler h ts
jsonBodyHandler =
requestBody @MyRecord JSON errorHandler $
proc request -> do
let record :: MyRecord = pick @(Body JSON MyRecord) $ from request
....
WebGear includes more MIME types apart from JSON
. These are defined in the WebGear.Core.MIMETypes
module:
HTML
- Thetext/html
MIME typePlainText
- Thetext/plain
MIME typeOctetStream
- Theapplication/octet-stream
MIME typeFormURLEncoded
- Theapplication/x-www-form-urlencoded
MIME typeFormData
- Themultipart/form-data
MIME type
Parsing the HTTP body to a Haskell value is defined through the BodyUnrender
type class defined in the WebGear.Server.MIMETypes
module. Instances of this type class includes the logic to parse the body according to the
rules of the MIME type.
Summary
In this chapter, you learned about the commonly used traits and middlewares to extract information from requests. You may find more information about these in their API documentation.
The traits and middlewares included in WebGear are sufficient for most common tasks. If you encounter a use case which is not covered by these traits, it is possible to implement your own traits to handle such cases. We will explore this in a subsequent chapter.