Skip to content

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 - The text/html MIME type
  • PlainText - The text/plain MIME type
  • OctetStream - The application/octet-stream MIME type
  • FormURLEncoded - The application/x-www-form-urlencoded MIME type
  • FormData - The multipart/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.