1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE DeriveFunctor #-}
3 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE Rank2Types #-}
6 {-# LANGUAGE StrictData #-}
7 {-# LANGUAGE TypeFamilies #-}
8 {-# OPTIONS_GHC -fno-warn-orphans #-}
9 module Symantic.HTTP.Client.Connection where
11 import Control.Applicative (Applicative(..), Alternative(..))
12 import Control.Monad (Monad(..), unless)
13 import Control.Monad.Trans.Class (MonadTrans(..))
15 import Data.Default.Class (Default(..))
16 import Data.Either (Either(..))
17 import Data.Eq (Eq(..))
18 import Data.Foldable (null, for_, toList, any)
19 import Data.Function (($), (.), on)
20 import Data.Functor (Functor, (<$>))
21 import Data.Maybe (Maybe(..), maybe)
22 import Data.Ord (Ord(..))
23 import Data.Proxy (Proxy(..))
24 import Data.Semigroup (Semigroup(..))
25 import Data.Sequence (Seq)
26 import Data.String (IsString(..))
27 import Data.Text (Text)
28 import Data.Tuple (fst)
30 import Text.Read (readMaybe)
31 import Text.Show (Show(..))
32 import qualified Control.Concurrent.STM as STM
33 import qualified Control.Exception as Exn
34 import qualified Control.Monad.Classes as MC
35 import qualified Control.Monad.Trans.Except as E
36 import qualified Control.Monad.Trans.Reader as R
37 import qualified Data.ByteString as BS
38 import qualified Data.ByteString.Builder as BSB
39 import qualified Data.ByteString.Lazy as BSL
40 import qualified Data.List as List
41 import qualified Data.Sequence as Seq
42 import qualified Data.Text as T
43 import qualified Data.Time.Clock as Time
44 import qualified Network.HTTP.Client as Client
45 import qualified Network.HTTP.Media as Media
46 import qualified Network.HTTP.Types as HTTP
47 import qualified Network.URI as URI
49 import Symantic.HTTP.Mime
50 import Symantic.HTTP.URI
52 -- * Type 'ClientConnection'
53 -- [ A monadic connection for a client query a server.
54 newtype ClientConnection a = ClientConnection { unClientConnection ::
56 (E.ExceptT ClientError IO)
58 } deriving (Functor, Applicative, Monad)
59 type instance MC.CanDo ClientConnection (MC.EffReader ClientEnv) = 'True
60 type instance MC.CanDo ClientConnection (MC.EffExcept ClientError) = 'True
61 type instance MC.CanDo ClientConnection (MC.EffExec IO) = 'True
62 instance MC.MonadExceptN 'MC.Zero ClientError ClientConnection where
63 throwN px = ClientConnection . lift . MC.throwN px
64 instance MC.MonadReaderN 'MC.Zero ClientEnv ClientConnection where
65 askN px = ClientConnection $ MC.askN px
66 instance MC.MonadExecN 'MC.Zero IO ClientConnection where
67 execN _px = ClientConnection . lift . lift
69 -- | Try clients in order, last error is preserved.
70 instance Alternative ClientConnection where
71 empty = MC.throw $ ClientError_EmptyClient
72 x <|> y = ClientConnection $ do
74 liftIO (runClientConnection env x) >>= \case
76 Left _err -> unClientConnection y
81 (Proxy (a,mt) -> ClientRequest) ->
83 clientConnection req = do
84 clientRes <- doClientRequest $ req (Proxy::Proxy (a,mt))
85 clientResMimeDecode (Proxy::Proxy mt) clientRes
87 runClientConnection :: ClientEnv -> ClientConnection a -> IO (Either ClientError a)
88 runClientConnection env (ClientConnection c) = E.runExceptT $ R.runReaderT c env
90 runClientRequest :: ClientEnv -> ClientRequest -> IO (Either ClientError ClientResponse)
91 runClientRequest env = runClientConnection env . doClientRequest
93 -- ** Type 'ClientEnv'
96 { clientEnv_manager :: Client.Manager
97 , clientEnv_baseURI :: URI
98 , clientEnv_cookieJar :: Maybe (STM.TVar Client.CookieJar)
100 clientEnv :: Client.Manager -> URI -> ClientEnv
101 clientEnv clientEnv_manager clientEnv_baseURI =
103 { clientEnv_cookieJar = Nothing
107 -- ** Type 'ClientError'
109 -- | The server returned an error response
110 = ClientError_FailureResponse ClientResponse
111 -- | The body could not be decoded at the expected type
112 | ClientError_DecodeFailure Text ClientResponse
113 -- | The content-type of the response is not supported
114 | ClientError_UnsupportedContentType MediaType ClientResponse
115 -- | The content-type header is invalid
116 | ClientError_InvalidContentTypeHeader ClientResponse
117 -- | There was a connection error, and no response was received
118 | ClientError_ClientConnectionectionError Client.HttpException
119 -- | 'ClientConnection' is 'empty'
120 | ClientError_EmptyClient
121 deriving (Eq, Show{-, Generic, Typeable-})
122 instance Exn.Exception ClientError
123 instance Eq Client.HttpException where
124 (==) = (==) `on` show
126 -- * Type 'ClientRequest'
129 { clientReqHttpVersion :: HTTP.HttpVersion
130 , clientReqMethod :: HTTP.Method
131 , clientReqPath :: BSB.Builder
132 , clientReqQueryString :: Seq HTTP.QueryItem
133 , clientReqAccept :: Seq Media.MediaType
134 , clientReqHeaders :: Seq HTTP.Header
135 , clientReqBody :: Maybe (Client.RequestBody, Media.MediaType)
137 instance Default ClientRequest where
139 { clientReqHttpVersion = HTTP.http11
140 , clientReqMethod = HTTP.methodGet
142 , clientReqQueryString = Seq.empty
143 , clientReqAccept = Seq.empty
144 , clientReqHeaders = Seq.empty
145 , clientReqBody = Nothing
147 instance Show ClientRequest where
148 show _ = "ClientRequest"
150 clientRequest :: URI -> ClientRequest -> Client.Request
151 clientRequest baseURI req =
152 Client.defaultRequest
153 { Client.method = clientReqMethod req
154 , Client.host = maybe "" (fromString . URI.uriRegName) $ URI.uriAuthority baseURI
155 , Client.port = case URI.uriPort <$> URI.uriAuthority baseURI of
156 Just (':':p) | Just port <- readMaybe p -> port
158 , Client.path = BSL.toStrict $ fromString (URI.uriPath baseURI) <> BSB.toLazyByteString (clientReqPath req)
159 , Client.queryString = HTTP.renderQuery True . toList $ clientReqQueryString req
160 , Client.requestHeaders = acceptHeader <> contentTypeHeader <> headers
162 , Client.secure = URI.uriScheme baseURI == "https"
165 headers = List.filter (\(h, _) -> h/="Accept" && h/="Content-Type") $
166 toList $ clientReqHeaders req
168 acceptHeader | null hs = []
169 | otherwise = [("Accept", Media.renderHeader hs)]
171 hs = toList $ clientReqAccept req
173 (requestBody, contentTypeHeader) =
174 case clientReqBody req of
175 Nothing -> (Client.RequestBodyLBS "", [])
176 Just (body, typ) -> (body, [(HTTP.hContentType, Media.renderHeader typ)])
178 setClientRequestBodyLBS :: BSL.ByteString -> MediaType -> ClientRequest -> ClientRequest
179 setClientRequestBodyLBS body mt req = req{ clientReqBody =
180 Just (Client.RequestBodyLBS body, mt) }
182 setClientRequestBody :: Client.RequestBody -> MediaType -> ClientRequest -> ClientRequest
183 setClientRequestBody body mt req = req{ clientReqBody = Just (body, mt) }
185 -- ** Type 'ClientResponse'
187 = ClientResponseWithBody BSL.ByteString
188 data ClientResponseWithBody a
190 { clientResStatus :: HTTP.Status
191 , clientResHeaders :: Seq HTTP.Header
192 , clientResHttpVersion :: HTTP.HttpVersion
194 } deriving (Eq, Show, Functor)
196 clientResponse :: Client.Response a -> ClientResponseWithBody a
199 { clientResStatus = Client.responseStatus res
200 , clientResBody = Client.responseBody res
201 , clientResHeaders = Seq.fromList $ Client.responseHeaders res
202 , clientResHttpVersion = Client.responseVersion res
205 doClientRequest :: ClientRequest -> ClientConnection ClientResponse
206 doClientRequest clientReq = do
207 ClientEnv{..} <- MC.ask
209 let req = clientRequest clientEnv_baseURI clientReq in
210 case clientEnv_cookieJar of
214 now <- Time.getCurrentTime
216 oldCookieJar <- STM.readTVar cj
217 let (newRequest, newCookieJar) =
218 Client.insertCookiesIntoRequest req oldCookieJar now
219 STM.writeTVar cj newCookieJar
222 liftIO $ catchClientConnectionectionError $
223 Client.httpLbs req clientEnv_manager
225 Left err -> MC.throw err
227 for_ clientEnv_cookieJar $ \cj ->
229 now <- Time.getCurrentTime
230 STM.atomically $ STM.modifyTVar' cj (fst . Client.updateCookieJar res req now)
231 let status = HTTP.statusCode $ Client.responseStatus res
232 clientRes = clientResponse res
233 unless (status >= 200 && status < 300) $
234 MC.throw $ ClientError_FailureResponse clientRes
237 catchClientConnectionectionError :: IO a -> IO (Either ClientError a)
238 catchClientConnectionectionError action =
239 Exn.catch (Right <$> action) $ \err ->
240 return $ Left $ ClientError_ClientConnectionectionError err
242 -- ** Type 'ClientResponseStreaming'
243 newtype ClientResponseStreaming
244 = ClientResponseStreaming
245 { runClientResponseStreaming ::
247 (ClientResponseWithBody (IO BS.ByteString) -> IO a) ->
251 doClientRequestStreaming :: ClientRequest -> ClientConnection ClientResponseStreaming
252 doClientRequestStreaming clientReq = do
253 ClientEnv{..} <- MC.ask
254 let req = clientRequest clientEnv_baseURI clientReq
255 return $ ClientResponseStreaming $ \k ->
256 Client.withResponse req clientEnv_manager $ \res -> do
257 let status = HTTP.statusCode $ Client.responseStatus res
258 unless (status >= 200 && status < 300) $ do
259 responseBody <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)
260 Exn.throw $ ClientError_FailureResponse $ clientResponse res{Client.responseBody}
261 k $ clientResponse res
263 clientResContentType ::
264 MC.MonadExcept ClientError m =>
265 ClientResponse -> m MediaType
266 clientResContentType clientRes =
267 case List.lookup "Content-Type" $ toList $ clientResHeaders clientRes of
268 Nothing -> return $ "application"Media.//"octet-stream"
270 case Media.parseAccept mt of
271 Nothing -> MC.throw $ ClientError_InvalidContentTypeHeader clientRes
272 Just mt' -> return mt'
274 clientResMimeDecode ::
275 MimeDecodable a mt =>
276 MC.MonadExcept ClientError m =>
277 Proxy mt -> ClientResponse -> m a
278 clientResMimeDecode mt clientRes = do
279 mtRes <- clientResContentType clientRes
280 unless (any (Media.matches mtRes) $ mimeTypes mt) $
281 MC.throw $ ClientError_UnsupportedContentType mtRes clientRes
282 case mimeDecode mt $ clientResBody clientRes of
283 Left err -> MC.throw $ ClientError_DecodeFailure (T.pack err) clientRes
284 Right val -> return val
287 liftIO :: MC.MonadExec IO m => IO a -> m a
289 {-# INLINE liftIO #-}