Simple Request/Response Model

The simplest possible conception of an http request in Haskell would be a function of the form Request a -> IO (Response b). In Dormouse, we seek to deviate as little as possible from that form and where we do, we do so only to support additional expressiveness and safety against constructing incorrect requests.


Dormouse's representation of HTTP requests look like this:

data HttpRequest url method body contentTag acceptTag = HttpRequest 
  { requestMethod :: !(HttpMethod method)
  , requestUrl :: !url
  , requestHeaders :: Map.Map HeaderName ByteString
  , requestBody :: body

Hopefully there is nothing too surprising going on here, an HTTP request is an entity consisting of a request method, url, a collection of headers and a content body. Let's take a look at each of the type parameters:

Let's also introduce some Dormouse defined tyes:

Note: Serialisation of the request is handled by a multi-parameter type class which considers the body of the response, i.e. the structure of the data, and the contentTag which describes the format the data should be serialised in.

Request Building

Dormouse provides a series of functions that can be used to create and incrementally transform an HTTP request towards a finalised form. Below, we describe a suggested path for convenient construction of

Step one: request method and URL

Dormouse defines some convenient helper functions for creating request templates for each HTTP Verb against a supplied Url.

Here is the one for post:

post :: IsUrl url => url -> HttpRequest url "POST" Empty EmptyPayload acceptTag   

Example use:

r :: HttpRequest (Url "https") "POST" Empty EmptyPayload acceptTag
r = post [https||]

This generates a basic HTTP POST request with no payload.

Step two: request body

If we are using an HTTP method like POST that permits request bodies, we can supply a request body using the supplyBody function.

supplyBody :: (AllowedBody method b, RequestPayload b contentTag) 
           => Proxy contentTag 
           -> b 
           -> HttpRequest url method b' contentTag' acceptTag 
           -> HttpRequest url method b contentTag acceptTag

The supply function transforms the initial request in to ways:

  1. It adds the supplied request body to the request.
  2. It adds a Content-Type header to the request based on the supplied contentTag.

Example use:

r' :: HttpRequest (Url "https") "POST" String JsonPayload acceptTag
r' = supplyBody json ("Test" :: String) $ post [https||]

In this example, a Content-Type: application/json header would be added to the request.

Step three: supply an accept header

We can supply an Accept header in our request using the accept function.

accept :: HasMediaType acceptTag 
       => Proxy acceptTag 
       -> HttpRequest url method b contentTag acceptTag 
       -> HttpRequest url method b contentTag acceptTag

This function fixes the acceptTag phantom type and sets a corresponding Accept header.

While this function is attached to the request, it is primarily useful because it allows us to introduce expectations at the type level about what kind of response we expect to receive.

Example use:

r'' :: HttpRequest (Url "https") "POST" String JsonPayload JsonPayload
r'' = accept json $ supplyBody json ("Test" :: String) $ post [https||]


Dormouse's representation of HTTP responses look like this:

data HttpResponse body = HttpResponse
  { responseStatusCode :: !Int
  , responseHeaders :: Map.Map HeaderName SB.ByteString
  , responseBody :: body

I think this is mostly self explanatory but the body type parameter allows us to support arbitrary data in the content body of the response.

Getting an HTTP response in Dormouse is most often achieved by the expect function. Let's take a look at its type signature

expect :: (MonadDormouseClient m, RequestPayload b contentTag, ResponsePayload b' acceptTag, IsUrl url)
       => HttpRequest url method b contentTag acceptTag -> m (HttpResponse b')

This function does several things: