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.
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:
The
urltype parameter tags the request with the Url scheme such that we can define functions that shoud accept strictlyhttporhttps; or either of the above.The
methodtype parameter tags the request with the name of the HTTP verb, this is used, for example to define functions that will not type-check if you attempt to supply a request body to a request method that does not allow them, e.g."GET".The
bodytype parameter allows us to support arbitrary data in the content body of the request.The
contentTagandacceptTagtype parameters are a little more complicated, these are phantom types that are used to indicate media types in the requestContent-TypeandAcceptheaders. We will see how they are used later!
Let's also introduce some Dormouse defined tyes:
HeaderNameis simply a type alias forCI ByteString(a case-insensitive Strict ByteString).HttpMethod methodis a GADT with all your standard HTTP Verbs (e.g.GET :: HttpMethod "GET") defined as well as support for custom methods.
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:
Example use:
r :: HttpRequest (Url "https") "POST" Empty EmptyPayload acceptTag
r = post [https|https://postman-echo.com/post|]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 acceptTagThe supply function transforms the initial request in to ways:
- It adds the supplied request body to the request.
- It adds a
Content-Typeheader to the request based on the suppliedcontentTag.
Example use:
r' :: HttpRequest (Url "https") "POST" String JsonPayload acceptTag
r' = supplyBody json ("Test" :: String) $ post [https|https://postman-echo.com/post|]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 acceptTagThis 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|https://postman-echo.com/post|]Responses
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:
- Serialises the arbitrary data in the HTTP request's content body into a stream of
Word8s. - Delegates sending the HTTP request, including the content body stream of
Word8s to someMonadDormouseClient m - Receives the HTTP response, including a content body stream of
Word8s. - Deserialises the HTTP responses' content body stream of
Word8s into some expected typeb'.